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.
- package/README.md +7 -5
- package/package.json +3 -3
- package/schemas/efficiency-config.schema.json +6 -0
- package/src/ask-dialog.ts +9 -2
- package/src/ask-user.ts +142 -74
- package/src/bootstrap.ts +2 -1
- package/src/compact-coordinator.ts +207 -35
- package/src/compact-economics-core.ts +62 -29
- package/src/config-catalog-core.ts +1 -0
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +40 -15
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +17 -6
- package/src/efficiency-store.ts +43 -6
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +141 -36
- package/src/history-store.ts +2 -0
- package/src/index-gates.ts +6 -12
- package/src/index-master.ts +12 -16
- package/src/index-pipe.ts +6 -0
- package/src/index-runtime.ts +14 -0
- package/src/index-worker.ts +4 -10
- package/src/index.ts +310 -76
- package/src/jev-client.ts +32 -7
- package/src/jev-core.ts +71 -36
- package/src/observation-core.ts +13 -1
- package/src/panel-open.ts +47 -0
- package/src/pier-options.ts +1 -2
- package/src/pipe-channel.ts +9 -0
- package/src/plugins/heat-plan.ts +18 -3
- package/src/plugins/observation.ts +118 -46
- package/src/plugins/subagent.ts +119 -68
- package/src/plugins/terminal.ts +221 -57
- package/src/plugins/todo.ts +63 -63
- package/src/reducer-core.ts +3 -5
- package/src/reducer-invoker.ts +27 -13
- package/src/renderers.ts +25 -2
- package/src/role-manifest.ts +2 -3
- package/src/roles/worker-default.json +2 -2
- package/src/settle-wake-core.ts +17 -3
- package/src/subagent-core.ts +209 -3
- package/src/subagent-poller.ts +69 -19
- package/src/subagent-session.ts +67 -4
- package/src/subagent-spawn.ts +55 -7
- package/src/terminal-core.ts +178 -50
- package/src/todo-core.ts +47 -2
- package/src/todo-read-hook.ts +54 -17
- package/src/todo-reminder-core.ts +9 -5
- package/src/todo-window.ts +8 -4
- package/src/todos-service.ts +12 -0
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +7 -1
- package/src/worker-marker.ts +76 -0
- package/src/focus-poller.ts +0 -223
package/src/herdr-client.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal herdr socket API client used inside the pi extension.
|
|
3
3
|
*
|
|
4
|
-
* Validated against herdr 0.9.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Validated against herdr 0.9.5 (protocol 22; see WIRE.md — unchanged since 0.9.1; 0.9.2 removed only the
|
|
5
|
+
* pane.graphics.* API, which pier never used). The request shapes pier builds are pinned to the
|
|
6
|
+
* 0.9.5 methods snapshot in test/fixtures/herdr-contract.json (recent `api schema` releases no longer
|
|
7
|
+
* emit a methods map) — after a herdr upgrade, re-verify the protocol number and the method set, then
|
|
8
|
+
* regenerate the fixture and bump HERDR_PROTOCOL_EXPECTED when the server moves on:
|
|
7
9
|
* - Transport is NDJSON: request {id, method, params} → response {id, result} | {id, error:{code,message}};
|
|
8
10
|
* - **Control requests use one connection per request**; the server closes it after replying;
|
|
9
11
|
* - Windows targets are named pipes whose name is the complete socket_path with a \\.\pipe\ prefix;
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
*/
|
|
19
21
|
import { REPORT_AGENT_SOURCE, type TodoItem } from './vocab.ts';
|
|
20
22
|
import { BLOCKED_LABEL_KEY, SIDEBAR_ASK_TOKEN, formatBlockedLabel, formatPaneTitle, sidebarTodoTokens, staleTokenClearance } from './pane-title.ts';
|
|
23
|
+
import { applyWorkerTag, delegationSidebarTokens, type DelegationContext } from './delegation-core.ts';
|
|
21
24
|
import { LOCK_BATCH_LIMIT, LOCK_TTL_MS } from './lock-core.ts';
|
|
22
25
|
import * as net from 'node:net';
|
|
23
26
|
|
|
@@ -57,6 +60,18 @@ export interface PaneListItem {
|
|
|
57
60
|
terminalTitleStripped?: string | null;
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/** pane.process_info: the processes currently holding the pane's PTY foreground. */
|
|
64
|
+
export interface PaneProcessInfo {
|
|
65
|
+
paneId: string;
|
|
66
|
+
shellPid: number | null;
|
|
67
|
+
foreground: ReadonlyArray<{
|
|
68
|
+
name: string | null;
|
|
69
|
+
argv0: string | null;
|
|
70
|
+
pid: number | null;
|
|
71
|
+
cwd: string | null;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
export interface PaneLayoutCell {
|
|
61
76
|
paneId: string;
|
|
62
77
|
x: number;
|
|
@@ -88,6 +103,8 @@ export interface OpenPluginPaneOptions {
|
|
|
88
103
|
|
|
89
104
|
export type OpenPluginPaneResult =
|
|
90
105
|
| { mode: 'popup'; ok: true }
|
|
106
|
+
/** Another popup/modal owns the session; the caller may switch that panel instead of opening a tab. */
|
|
107
|
+
| { mode: 'busy' }
|
|
91
108
|
| { mode: 'pane'; paneId: string }
|
|
92
109
|
| { mode: 'fallback_tab'; tabId?: string; paneId?: string };
|
|
93
110
|
|
|
@@ -127,12 +144,23 @@ export function herdrUnavailableHint(err: unknown): string | null {
|
|
|
127
144
|
return `herdr unreachable (${msg}) — check that the herdr server is running and HERDR_SOCKET_PATH matches its socket`;
|
|
128
145
|
}
|
|
129
146
|
|
|
130
|
-
/** Wire protocol this client is written against (herdr 0.9.
|
|
147
|
+
/** Wire protocol this client is written against (herdr 0.9.5, contract fixture regenerated 2026-10-20). */
|
|
131
148
|
export const HERDR_PROTOCOL_EXPECTED = 22;
|
|
132
149
|
|
|
150
|
+
export type WaitFailureReason = 'timeout' | 'unavailable';
|
|
151
|
+
|
|
133
152
|
export type WaitForOutputResult =
|
|
134
153
|
| { matched: true }
|
|
135
|
-
| { matched: false; reason:
|
|
154
|
+
| { matched: false; reason: WaitFailureReason };
|
|
155
|
+
|
|
156
|
+
/** Classify a failed `pane.wait_for_output`: null means the error is not a wait outcome and should
|
|
157
|
+
* surface. herdr may report its own server-side timeout under an `unavailable` code or as
|
|
158
|
+
* "timed out"; a wait that consumed its budget is a timeout regardless of the label it chose. */
|
|
159
|
+
export function classifyWaitFailure(message: string, elapsedMs: number, timeoutMs: number): WaitFailureReason | null {
|
|
160
|
+
if (/timeout|timed out|deadline exceeded/i.test(message) || elapsedMs >= timeoutMs * 0.95) return 'timeout';
|
|
161
|
+
if (/not supported|not implemented|unknown method|unavailable|method not found/i.test(message)) return 'unavailable';
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
136
164
|
|
|
137
165
|
/** pane.read / agent.read selection: `recent` keeps ANSI so T6 fullscreen detection still sees it. */
|
|
138
166
|
export interface ReadOptions {
|
|
@@ -151,8 +179,7 @@ export interface ReadBuffer {
|
|
|
151
179
|
export interface HerdrClientLike {
|
|
152
180
|
readonly available: boolean;
|
|
153
181
|
reportAgent(state: PaneAgentState, message: string | null): Promise<void>;
|
|
154
|
-
reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
155
|
-
/** M18: report write-lock tokens (lock-<hash> → paneId|path); null releases, and batches stay within 16 keys. */
|
|
182
|
+
reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null; workerTag?: string | null }): Promise<void>;
|
|
156
183
|
reportLockTokens(tokens: Record<string, string | null>): Promise<void>;
|
|
157
184
|
/** D93: sidebar agent display name (display_agent is the role name); null clears it. Persists, no TTL. */
|
|
158
185
|
reportDisplayAgent(name: string | null): Promise<void>;
|
|
@@ -161,6 +188,14 @@ export interface HerdrClientLike {
|
|
|
161
188
|
* grade blocked + pi-ask as the ask level, distinct from plain block.
|
|
162
189
|
*/
|
|
163
190
|
reportAskFlag(text: string | null): Promise<void>;
|
|
191
|
+
/** Delegation identity tokens (pi-parent/pi-task): who delegated this pane and for what task. */
|
|
192
|
+
reportDelegationTokens(d: DelegationContext | null): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* herdr 0.9.2+ self-reported restore: register the command that reopens this exact session after
|
|
195
|
+
* a server restart. Worker panes only; masters stay with herdr's native pi integration.
|
|
196
|
+
* Silently no-ops on older servers and on any error — resume is always best-effort.
|
|
197
|
+
*/
|
|
198
|
+
reportAgentSessionResume(sessionId: string, resumeArgv: readonly string[]): Promise<void>;
|
|
164
199
|
listAgents(): Promise<AgentInfo[]>;
|
|
165
200
|
/** Inject text into a pane terminal (pi editor input + Enter); the PTY channel is used only to start launchLine. */
|
|
166
201
|
sendPaneText(paneId: string, text: string): Promise<void>;
|
|
@@ -173,6 +208,8 @@ export interface HerdrClientLike {
|
|
|
173
208
|
closePane(paneId: string): Promise<void>;
|
|
174
209
|
createTab(opts: { workspaceId: string; label?: string; cwd?: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
175
210
|
listPanes(): Promise<PaneListItem[]>;
|
|
211
|
+
/** Foreground process snapshot; null when the server predates the method or the pane is gone. */
|
|
212
|
+
processInfo(paneId: string): Promise<PaneProcessInfo | null>;
|
|
176
213
|
/** D91: layout tree by paneId or tabId; null on failure. */
|
|
177
214
|
exportLayout(opts?: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null>;
|
|
178
215
|
/** 0.9.1: live pane rects for the tab containing paneId. Best effort returns null. */
|
|
@@ -183,7 +220,6 @@ export interface HerdrClientLike {
|
|
|
183
220
|
/** 0.9.1: Launch a manifest pane entrypoint with placement (popup/tab/split/overlay/zoomed). */
|
|
184
221
|
openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult>;
|
|
185
222
|
agentExplain(target: string): Promise<Record<string, unknown> | null>;
|
|
186
|
-
getServerVersion(): Promise<string | null>;
|
|
187
223
|
/** pane.send_keys; herdr key combos include ctrl+c, enter, and esc. */
|
|
188
224
|
sendPaneKeys(paneId: string, keys: string[]): Promise<void>;
|
|
189
225
|
readPane(paneId: string, opts?: ReadOptions): Promise<ReadBuffer>;
|
|
@@ -212,8 +248,10 @@ function findIdIn(obj: unknown, key: string, depth = 0): string | null {
|
|
|
212
248
|
export class NoopHerdrClient implements HerdrClientLike {
|
|
213
249
|
readonly available = false;
|
|
214
250
|
async reportAgent(): Promise<void> {}
|
|
251
|
+
async reportAgentSessionResume(): Promise<void> {}
|
|
215
252
|
async reportMetadata(): Promise<void> {}
|
|
216
253
|
async reportLockTokens(): Promise<void> {}
|
|
254
|
+
async reportDelegationTokens(): Promise<void> {}
|
|
217
255
|
async reportDisplayAgent(): Promise<void> {}
|
|
218
256
|
async reportAskFlag(): Promise<void> {}
|
|
219
257
|
async sendPaneText(): Promise<void> {}
|
|
@@ -223,13 +261,13 @@ export class NoopHerdrClient implements HerdrClientLike {
|
|
|
223
261
|
close(): void {}
|
|
224
262
|
async listAgents(): Promise<AgentInfo[]> { return []; }
|
|
225
263
|
async listPanes(): Promise<PaneListItem[]> { return []; }
|
|
264
|
+
async processInfo(): Promise<PaneProcessInfo | null> { return null; }
|
|
226
265
|
async tabList(): Promise<TabInfo[]> { return []; }
|
|
227
266
|
async waitAgent(): Promise<null> { return null; }
|
|
228
267
|
async getAgentSessionPath(): Promise<null> { return null; }
|
|
229
268
|
async exportLayout(): Promise<null> { return null; }
|
|
230
269
|
async paneLayout(): Promise<null> { return null; }
|
|
231
270
|
async agentExplain(): Promise<null> { return null; }
|
|
232
|
-
async getServerVersion(): Promise<null> { return null; }
|
|
233
271
|
async readPane(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
234
272
|
async readAgent(): Promise<ReadBuffer> { return { text: '', revision: 0, truncated: false }; }
|
|
235
273
|
async waitForOutput(): Promise<WaitForOutputResult> { return { matched: false, reason: 'unavailable' }; }
|
|
@@ -312,6 +350,13 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
312
350
|
|
|
313
351
|
/* ── Reporting (silent best effort: the workbench mirror must never affect pi's main flow) ── */
|
|
314
352
|
|
|
353
|
+
/**
|
|
354
|
+
* D-2 (verified 2026-10-20): herdr's native `herdr:pi` integration owns `agent_status`
|
|
355
|
+
* (`screen_detection_skip_reason: full_lifecycle_hook_authority`), but it never reports the
|
|
356
|
+
* activity text. `pane.report_agent` is still the only carrier for the tool badge, so pier keeps
|
|
357
|
+
* sending state with the message; removing it requires migrating the badge to
|
|
358
|
+
* `report_metadata.state_labels` first.
|
|
359
|
+
*/
|
|
315
360
|
async reportAgent(state: PaneAgentState, message: string | null): Promise<void> {
|
|
316
361
|
try {
|
|
317
362
|
await this.request('pane.report_agent', {
|
|
@@ -325,16 +370,47 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
325
370
|
}
|
|
326
371
|
}
|
|
327
372
|
|
|
328
|
-
|
|
329
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Master panes stay with herdr's native pi integration (it owns the session path); workers
|
|
375
|
+
* self-report so a restart replays pier's own launch line — extension loaded, role model, approve,
|
|
376
|
+
* fullscreen frame — instead of a bare `pi --resume` without `-e`. herdr requires the source to
|
|
377
|
+
* hold the pane before accepting a resume command (resume_not_accepted), so an idle state report
|
|
378
|
+
* goes first, with one retry to tolerate a lost race with the session_start gate report.
|
|
379
|
+
*/
|
|
380
|
+
async reportAgentSessionResume(sessionId: string, resumeArgv: readonly string[]): Promise<void> {
|
|
381
|
+
if (!this.env) return;
|
|
382
|
+
try {
|
|
383
|
+
await this.reportAgent('idle', null);
|
|
384
|
+
const params = {
|
|
385
|
+
pane_id: this.env.paneId,
|
|
386
|
+
source: REPORT_AGENT_SOURCE,
|
|
387
|
+
agent: 'pi',
|
|
388
|
+
agent_session_id: sessionId,
|
|
389
|
+
resume_argv: [...resumeArgv],
|
|
390
|
+
};
|
|
391
|
+
try {
|
|
392
|
+
await this.request('pane.report_agent_session', params);
|
|
393
|
+
} catch {
|
|
394
|
+
const { promise: retryDelay, resolve: retryNow } = Promise.withResolvers<void>();
|
|
395
|
+
setTimeout(retryNow, 250);
|
|
396
|
+
await retryDelay;
|
|
397
|
+
await this.request('pane.report_agent_session', params);
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
/* best effort: live sessions never depend on restore metadata */
|
|
401
|
+
}
|
|
402
|
+
}
|
|
330
403
|
|
|
331
|
-
async reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
404
|
+
async reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null; workerTag?: string | null }): Promise<void> {
|
|
332
405
|
try {
|
|
333
406
|
void meta.session;
|
|
334
|
-
const title =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
407
|
+
const title = applyWorkerTag(
|
|
408
|
+
formatPaneTitle(meta.items, null, {
|
|
409
|
+
progressSuffix: meta.progressSuffix,
|
|
410
|
+
lastWriteAt: meta.lastWriteAt,
|
|
411
|
+
}),
|
|
412
|
+
meta.workerTag ?? null,
|
|
413
|
+
);
|
|
338
414
|
const blocked = formatBlockedLabel(meta.items);
|
|
339
415
|
// D96: stale cleanup needs its own batch — stale(16) + pi-todo(1) exceeds herdr's 16-token
|
|
340
416
|
// limit, and the rejected request would drop both the title and the tokens.
|
|
@@ -362,6 +438,22 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
362
438
|
}
|
|
363
439
|
}
|
|
364
440
|
|
|
441
|
+
/** RFC rfc-delegation-identity §2.3: separate batch — tokens patches never merge with the stale
|
|
442
|
+
* cleanup or the todo title (D96 discipline); an unknown parent pane skips the report. */
|
|
443
|
+
async reportDelegationTokens(d: DelegationContext | null): Promise<void> {
|
|
444
|
+
const tokens = d ? delegationSidebarTokens(d) : {};
|
|
445
|
+
if (Object.keys(tokens).length === 0) return;
|
|
446
|
+
try {
|
|
447
|
+
await this.request('pane.report_metadata', {
|
|
448
|
+
pane_id: this.env.paneId,
|
|
449
|
+
source: REPORT_AGENT_SOURCE,
|
|
450
|
+
tokens,
|
|
451
|
+
ttl_ms: 86400000,
|
|
452
|
+
});
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
365
457
|
async reportLockTokens(tokens: Record<string, string | null>): Promise<void> {
|
|
366
458
|
const keys = Object.keys(tokens);
|
|
367
459
|
if (keys.length === 0) return;
|
|
@@ -513,6 +605,29 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
513
605
|
}));
|
|
514
606
|
}
|
|
515
607
|
|
|
608
|
+
async processInfo(paneId: string): Promise<PaneProcessInfo | null> {
|
|
609
|
+
try {
|
|
610
|
+
const result = (await this.request('pane.process_info', { pane_id: paneId })) as Record<string, unknown> | null;
|
|
611
|
+
const info = (result?.process_info ?? result) as Record<string, unknown> | null | undefined;
|
|
612
|
+
if (!info || typeof info !== 'object') return null;
|
|
613
|
+
const raw = Array.isArray(info.foreground_processes) ? info.foreground_processes : [];
|
|
614
|
+
return {
|
|
615
|
+
paneId: typeof info.pane_id === 'string' ? info.pane_id : paneId,
|
|
616
|
+
shellPid: typeof info.shell_pid === 'number' ? info.shell_pid : null,
|
|
617
|
+
foreground: raw
|
|
618
|
+
.filter((p): p is Record<string, unknown> => p !== null && typeof p === 'object')
|
|
619
|
+
.map((p) => ({
|
|
620
|
+
name: typeof p.name === 'string' ? p.name : null,
|
|
621
|
+
argv0: typeof p.argv0 === 'string' ? p.argv0 : null,
|
|
622
|
+
pid: typeof p.pid === 'number' ? p.pid : null,
|
|
623
|
+
cwd: typeof p.cwd === 'string' ? p.cwd : null,
|
|
624
|
+
})),
|
|
625
|
+
};
|
|
626
|
+
} catch {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
516
631
|
async exportLayout(opts: { paneId?: string; tabId?: string } = {}): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null> {
|
|
517
632
|
try {
|
|
518
633
|
const result = (await this.request('layout.export', {
|
|
@@ -608,8 +723,14 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
608
723
|
}
|
|
609
724
|
return { mode: 'pane', paneId: findIdIn(result, 'pane_id') ?? '' };
|
|
610
725
|
} catch (err) {
|
|
726
|
+
// A live popup/modal is a transient state, not an old server: opening a tab would orphan a
|
|
727
|
+
// second dashboard and silently change the surface. Callers decide whether to switch panels.
|
|
728
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
729
|
+
if (opts.placement === 'popup' && /ui_busy|popup pane is already open/i.test(message)) {
|
|
730
|
+
return { mode: 'busy' };
|
|
731
|
+
}
|
|
611
732
|
// 0.9.0 serde rejects unknown `popup` as "unknown variant" / invalid_params — not a dedicated
|
|
612
|
-
// invalid_placement code. Any popup failure retries as tab; tab failure surfaces to Level 3.
|
|
733
|
+
// invalid_placement code. Any other popup failure retries as tab; tab failure surfaces to Level 3.
|
|
613
734
|
if (opts.placement !== 'popup') throw err;
|
|
614
735
|
const { width: _width, height: _height, target_pane_id: _target, ...base } = params;
|
|
615
736
|
const fallbackRes = (await this.request('plugin.pane.open', { ...base, placement: 'tab' })) as Record<string, unknown> | null;
|
|
@@ -635,21 +756,6 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
635
756
|
}
|
|
636
757
|
}
|
|
637
758
|
|
|
638
|
-
private cachedVersion: string | null = null;
|
|
639
|
-
|
|
640
|
-
async getServerVersion(): Promise<string | null> {
|
|
641
|
-
if (this.cachedVersion) return this.cachedVersion;
|
|
642
|
-
try {
|
|
643
|
-
const result = (await this.request('ping', {})) as { version?: unknown } | null;
|
|
644
|
-
if (typeof result?.version === 'string') {
|
|
645
|
-
this.cachedVersion = result.version;
|
|
646
|
-
return result.version;
|
|
647
|
-
}
|
|
648
|
-
} catch {
|
|
649
|
-
}
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
759
|
async sendPaneKeys(paneId: string, keys: string[]): Promise<void> {
|
|
654
760
|
await this.request('pane.send_keys', { pane_id: paneId, keys });
|
|
655
761
|
}
|
|
@@ -688,6 +794,7 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
688
794
|
}
|
|
689
795
|
|
|
690
796
|
async waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult> {
|
|
797
|
+
const started = Date.now();
|
|
691
798
|
try {
|
|
692
799
|
await this.request('pane.wait_for_output', {
|
|
693
800
|
pane_id: paneId,
|
|
@@ -698,10 +805,8 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
698
805
|
return { matched: true };
|
|
699
806
|
} catch (err) {
|
|
700
807
|
const msg = String((err as Error)?.message ?? err);
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
return { matched: false, reason: 'unavailable' };
|
|
704
|
-
}
|
|
808
|
+
const reason = classifyWaitFailure(msg, Date.now() - started, timeoutMs);
|
|
809
|
+
if (reason != null) return { matched: false, reason };
|
|
705
810
|
throw err;
|
|
706
811
|
}
|
|
707
812
|
}
|
package/src/history-store.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface HistoryEntry {
|
|
|
29
29
|
kind: string;
|
|
30
30
|
paneId: string;
|
|
31
31
|
tabId: string;
|
|
32
|
+
/** Phase 2 task slug — carried so resume-from-history keeps the tool-facing reference stable. */
|
|
33
|
+
slug?: string;
|
|
32
34
|
tabName?: string;
|
|
33
35
|
workspaceId: string;
|
|
34
36
|
cwd: string;
|
package/src/index-gates.ts
CHANGED
|
@@ -8,10 +8,7 @@
|
|
|
8
8
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
9
9
|
import type { HerdrClientLike } from './herdr-client.ts';
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
type GatePi = ExtensionAPI & {
|
|
13
|
-
on?: (name: 'ui_prompt_start' | 'ui_prompt_end', handler: (event: unknown) => void) => void;
|
|
14
|
-
};
|
|
11
|
+
/** ui_prompt events are typed by pi ≥ 1.1 (they postdated the 0.86 pin this gate was written against). */
|
|
15
12
|
|
|
16
13
|
/** Only inherently human-blocking kinds open the gate; `custom` covers resident overlays too (D97). */
|
|
17
14
|
const GATING_PROMPT_KINDS = new Set(['select', 'confirm', 'input', 'editor']);
|
|
@@ -95,18 +92,15 @@ export function createGateRuntime(d: GateRuntimeDeps): GateRuntime {
|
|
|
95
92
|
}
|
|
96
93
|
});
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
const kind = typeof rec.kind === 'string' ? rec.kind : 'prompt';
|
|
103
|
-
if (!GATING_PROMPT_KINDS.has(kind)) return;
|
|
104
|
-
const label = title ?? kind;
|
|
95
|
+
d.pi.on('ui_prompt_start', (event) => {
|
|
96
|
+
const title = typeof event.title === 'string' && event.title.trim() ? event.title.trim() : null;
|
|
97
|
+
if (!GATING_PROMPT_KINDS.has(event.kind)) return;
|
|
98
|
+
const label = title ?? event.kind;
|
|
105
99
|
// The ask tool opens its own gate first, so this is usually a nested no-op;
|
|
106
100
|
// prompts from other extensions or pi core open the gate here.
|
|
107
101
|
if (enter(label)) emitHerdrBlocked(true, label);
|
|
108
102
|
});
|
|
109
|
-
|
|
103
|
+
d.pi.on('ui_prompt_end', () => {
|
|
110
104
|
if (exit()) emitHerdrBlocked(false, null);
|
|
111
105
|
});
|
|
112
106
|
|
package/src/index-master.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
5
5
|
import type { Server } from 'node:net';
|
|
6
6
|
import { PiSurface } from './pi-surface.ts';
|
|
7
7
|
import { createCordisApp } from './bootstrap.ts';
|
|
8
|
+
import { baseTodoDeps, cordisProvide } from './cordis-services.ts';
|
|
8
9
|
import { disposeSessionRoot } from './subagent-poller.ts';
|
|
9
10
|
import terminalPlugin from './plugins/terminal.ts';
|
|
10
11
|
import todoPlugin from './plugins/todo.ts';
|
|
@@ -15,7 +16,7 @@ import type { RoutingTelemetryRecord } from './routing-telemetry.ts';
|
|
|
15
16
|
import type { TodosService } from './todos-service.ts';
|
|
16
17
|
import type { TodoUiSlot } from './plugins/todo.ts';
|
|
17
18
|
import type { SubagentPortBox } from './subagent-core.ts';
|
|
18
|
-
import type {
|
|
19
|
+
import type { WorkerWidgetRow } from './tree-view.ts';
|
|
19
20
|
|
|
20
21
|
export interface MasterPluginMount {
|
|
21
22
|
pi: ExtensionAPI;
|
|
@@ -40,6 +41,8 @@ export interface MasterPluginMount {
|
|
|
40
41
|
jev?: { ask: JevRuntime['ask']; getMinConfidence: () => number };
|
|
41
42
|
/** Phase 0 routing telemetry append (RFC rfc-jev-role-routing §8); absent in tests. */
|
|
42
43
|
appendRoutingLog?: (row: RoutingTelemetryRecord) => void;
|
|
44
|
+
/** D113: running subagent rows joined with their cached todos — feeds the unified widget. */
|
|
45
|
+
getWorkers?: () => readonly WorkerWidgetRow[];
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
async function loadEntry(
|
|
@@ -74,25 +77,19 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
74
77
|
}, 'pipe-server');
|
|
75
78
|
|
|
76
79
|
const surface = new PiSurface(m.pi as unknown as object, cordisApp.ledger);
|
|
77
|
-
sessionRoot
|
|
80
|
+
cordisProvide(sessionRoot, 'pi-herdr.surface', surface);
|
|
78
81
|
const terminalDeps = {
|
|
79
82
|
client: m.client,
|
|
80
83
|
env: m.env,
|
|
81
84
|
state: { activePaneIds: (): Set<string> => new Set() },
|
|
82
85
|
};
|
|
83
|
-
sessionRoot
|
|
86
|
+
cordisProvide(sessionRoot, 'pi-herdr.terminal-deps', terminalDeps);
|
|
84
87
|
|
|
85
88
|
await loadEntry(sessionRoot, useLoader, './plugins/terminal.ts', terminalPlugin);
|
|
86
89
|
|
|
87
|
-
sessionRoot
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
maxItems: 15,
|
|
91
|
-
mirrorTodos: m.mirrorTodos,
|
|
92
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
93
|
-
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
94
|
-
},
|
|
95
|
-
state: m.todoUi,
|
|
90
|
+
cordisProvide(sessionRoot, 'pi-herdr.todo-deps', {
|
|
91
|
+
...baseTodoDeps(m),
|
|
92
|
+
...(m.getWorkers ? { getWorkers: m.getWorkers } : {}),
|
|
96
93
|
getBlockedDepth: m.getBlockedDepth,
|
|
97
94
|
stopReminder: {
|
|
98
95
|
getBlockedDepth: m.getBlockedDepth,
|
|
@@ -103,9 +100,8 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
103
100
|
});
|
|
104
101
|
await loadEntry(sessionRoot, useLoader, './plugins/todo.ts', todoPlugin);
|
|
105
102
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
sessionRoot.provide('pi-herdr.subagent-deps', {
|
|
103
|
+
// Dropped keys fail here: cordisProvide checks the bag against PierCordisServices (fe99252).
|
|
104
|
+
cordisProvide(sessionRoot, 'pi-herdr.subagent-deps', {
|
|
109
105
|
client: m.client,
|
|
110
106
|
env: m.env,
|
|
111
107
|
extPath: m.extPath,
|
|
@@ -120,7 +116,7 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
120
116
|
terminalState: terminalDeps.state,
|
|
121
117
|
...(m.jev ? { jev: m.jev } : {}),
|
|
122
118
|
...(m.appendRoutingLog ? { logRouting: m.appendRoutingLog } : {}),
|
|
123
|
-
}
|
|
119
|
+
});
|
|
124
120
|
await loadEntry(sessionRoot, useLoader, './plugins/subagent.ts', subagentPlugin);
|
|
125
121
|
|
|
126
122
|
m.pi.on('session_shutdown', () => {
|
package/src/index-pipe.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface PipeHandlerSession {
|
|
|
21
21
|
setPendingMachineRequest: (req: MachineRequest | null) => void;
|
|
22
22
|
/** P0 (RFC §4.4): apply a role switch requested over the pipe (master → worker). */
|
|
23
23
|
applyRoleSwitch: (role: string, switchedBy: string) => Promise<{ ok: boolean; message: string }>;
|
|
24
|
+
/** D113 (RFC rfc-unified-widget): worker → master full todo push; updates the widget cache. */
|
|
25
|
+
applyWorkerTodos: (workerPaneId: string, items: Array<{ content: string; status: string; blocker?: string }>, summary: string) => void;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export async function handlePipeRequest(
|
|
@@ -54,6 +56,10 @@ export async function handlePipeRequest(
|
|
|
54
56
|
? { type: 'ok', id: req.id, detail: res.message }
|
|
55
57
|
: { type: 'error', id: req.id, message: res.message };
|
|
56
58
|
}
|
|
59
|
+
case 'todo_sync': {
|
|
60
|
+
s.applyWorkerTodos(req.paneId, req.items, req.summary);
|
|
61
|
+
return { type: 'ok', id: req.id };
|
|
62
|
+
}
|
|
57
63
|
case 'reply': {
|
|
58
64
|
s.port.current?.applyReplySession(req.paneId, req.sessionFile);
|
|
59
65
|
// Consume before the finish notice: otherwise listRunningSubs still reports the pane and the
|
package/src/index-runtime.ts
CHANGED
|
@@ -21,3 +21,17 @@ export function planIndexMode(env: NodeJS.ProcessEnv = process.env): IndexMode {
|
|
|
21
21
|
composeMaster: hasHerdr && !isSubagent,
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* herdr 0.9.2 restore path: a reopened worker pane runs pier's resume_argv, which cannot carry the
|
|
27
|
+
* spawn env. A worker marker next to the session file is the durable identity, so the restored boot
|
|
28
|
+
* plans worker mode (todo-only mount, no master services) instead of master composition.
|
|
29
|
+
*/
|
|
30
|
+
export function applyRestoredWorkerMode(
|
|
31
|
+
mode: IndexMode,
|
|
32
|
+
sessionFile: string | null | undefined,
|
|
33
|
+
markerExists: (sessionFile: string) => boolean,
|
|
34
|
+
): IndexMode {
|
|
35
|
+
if (mode.isSubagent || !mode.hasHerdr || !sessionFile) return mode;
|
|
36
|
+
return markerExists(sessionFile) ? { isSubagent: true, hasHerdr: true, composeMaster: false } : mode;
|
|
37
|
+
}
|
package/src/index-worker.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
4
4
|
import { Context } from '@deepseek-ai/cordis';
|
|
5
5
|
import { PiSurface } from './pi-surface.ts';
|
|
6
|
+
import { baseTodoDeps, cordisProvide } from './cordis-services.ts';
|
|
6
7
|
import todoPlugin from './plugins/todo.ts';
|
|
7
8
|
import type { TodosService } from './todos-service.ts';
|
|
8
9
|
import type { TodoUiSlot } from './plugins/todo.ts';
|
|
@@ -26,16 +27,9 @@ export interface TodoOnlyMount {
|
|
|
26
27
|
export async function mountTodoOnly(m: TodoOnlyMount): Promise<void> {
|
|
27
28
|
const workerRoot = new Context();
|
|
28
29
|
const workerSurface = new PiSurface(m.pi as unknown as object);
|
|
29
|
-
workerRoot
|
|
30
|
-
workerRoot
|
|
31
|
-
|
|
32
|
-
allowParallelInProgress: m.todos.config.allowParallelInProgress,
|
|
33
|
-
maxItems: 15,
|
|
34
|
-
mirrorTodos: m.mirrorTodos,
|
|
35
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
36
|
-
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
37
|
-
},
|
|
38
|
-
state: m.todoUi,
|
|
30
|
+
cordisProvide(workerRoot, 'pi-herdr.surface', workerSurface);
|
|
31
|
+
cordisProvide(workerRoot, 'pi-herdr.todo-deps', {
|
|
32
|
+
...baseTodoDeps(m),
|
|
39
33
|
...(m.getBlockedDepth ? { getBlockedDepth: m.getBlockedDepth } : {}),
|
|
40
34
|
...(m.stopReminder ? { stopReminder: m.stopReminder } : {}),
|
|
41
35
|
});
|