pi-pier 0.2.6 → 0.3.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.
- package/README.md +20 -7
- package/package.json +7 -2
- package/schemas/efficiency-config.schema.json +77 -0
- package/skills/pi-herdr-workflow/SKILL.md +13 -1
- package/src/ansi-text.ts +83 -0
- package/src/ask-multi.ts +260 -0
- package/src/ask-user.ts +458 -0
- package/src/bootstrap.ts +5 -2
- package/src/compact-coordinator.ts +454 -0
- package/src/compact-economics-core.ts +396 -0
- package/src/config-catalog-core.ts +452 -0
- package/src/config-command.ts +181 -0
- package/src/config-guide.ts +361 -0
- package/src/core/grid-shape.ts +4 -2
- package/src/core/heat-plan.ts +160 -0
- package/src/core/observation.ts +555 -0
- package/src/core/subagent.ts +187 -393
- package/src/core/terminal.ts +270 -47
- package/src/core/todo.ts +49 -14
- package/src/dashboard-command.ts +89 -0
- package/src/efficiency-config-core.ts +661 -0
- package/src/efficiency-config-io.ts +53 -0
- package/src/efficiency-store.ts +312 -0
- package/src/focus-poller.ts +244 -0
- package/src/gc-core.ts +85 -0
- package/src/git-adapter.ts +12 -3
- package/src/herdr-client.ts +292 -36
- package/src/index-locks.ts +27 -9
- package/src/index-master.ts +5 -0
- package/src/index-notices.ts +13 -2
- package/src/index-pipe.ts +5 -0
- package/src/index-worker.ts +5 -0
- package/src/index.ts +360 -60
- package/src/jev-client.ts +146 -0
- package/src/jev-core.ts +381 -0
- package/src/lock-core.ts +105 -16
- package/src/observation-core.ts +248 -0
- package/src/pier-options.ts +96 -0
- package/src/pipe-channel.ts +52 -7
- package/src/reducer-core.ts +358 -0
- package/src/reducer-invoker.ts +392 -0
- package/src/renderers.ts +198 -0
- package/src/role-loader.ts +2 -1
- package/src/role-manifest.ts +0 -4
- package/src/runtime-policy.ts +5 -8
- package/src/session-tail.ts +60 -5
- package/src/settle-wake-core.ts +3 -1
- package/src/slim-frame.ts +60 -3
- package/src/storage-layout.ts +23 -0
- package/src/subagent-core.ts +3 -1
- package/src/subagent-execute.ts +328 -0
- package/src/subagent-gc.ts +33 -13
- package/src/subagent-launch.ts +4 -0
- package/src/subagent-list-action.ts +69 -0
- package/src/subagent-output-action.ts +104 -0
- package/src/subagent-output-adapter.ts +27 -0
- package/src/subagent-output-core.ts +263 -0
- package/src/subagent-poll-loop.ts +59 -39
- package/src/subagent-poller.ts +46 -0
- package/src/subagent-ready.ts +89 -0
- package/src/subagent-registry.ts +70 -0
- package/src/subagent-resolution.ts +41 -0
- package/src/subagent-scope.ts +3 -1
- package/src/subagent-session-io.ts +115 -32
- package/src/subagent-spawn.ts +153 -8
- package/src/swallow.ts +75 -0
- package/src/terminal-core.ts +161 -10
- package/src/todo-core.ts +5 -4
- package/src/todo-reminder-core.ts +8 -1
- package/src/todos-service.ts +18 -6
- package/src/tool-error.ts +26 -0
- package/src/vocab.ts +7 -4
package/src/git-adapter.ts
CHANGED
|
@@ -22,8 +22,17 @@ export interface GitError extends Error {
|
|
|
22
22
|
export type GitExecFile = (
|
|
23
23
|
file: string,
|
|
24
24
|
args: readonly string[],
|
|
25
|
-
options: { timeout: number; encoding: 'utf8' },
|
|
26
|
-
) => Promise<{ stdout: string; stderr: string }
|
|
25
|
+
options: { timeout: number; encoding: 'utf8'; maxBuffer: number },
|
|
26
|
+
) => Promise<{ stdout: string; stderr: string }>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Output ceiling for one git call. `execFile` defaults to 1MB, which a large checkout exceeds
|
|
30
|
+
* (`status --porcelain` on a repo with tens of thousands of untracked files, or `diff --stat`
|
|
31
|
+
* across a wide change): the call then fails with ENOBUFS, callers normalize it to null, and the
|
|
32
|
+
* user sees a silently missing stat line — or "failed to create worktree" for a worktree that was
|
|
33
|
+
* in fact created. 64MB bounds memory without hitting realistic repositories.
|
|
34
|
+
*/
|
|
35
|
+
const GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Minimal Git adapter for worktree and status operations.
|
|
@@ -79,7 +88,7 @@ export class NodeGitAdapter implements GitAdapter {
|
|
|
79
88
|
const { stdout, stderr } = await this.exec(
|
|
80
89
|
this.gitExecutable,
|
|
81
90
|
['-C', cwd, ...args],
|
|
82
|
-
{ timeout: this.timeoutMs, encoding: 'utf8' },
|
|
91
|
+
{ timeout: this.timeoutMs, encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER },
|
|
83
92
|
)
|
|
84
93
|
return { stdout, stderr }
|
|
85
94
|
} catch (err) {
|
package/src/herdr-client.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal herdr socket API client used inside the pi extension.
|
|
3
3
|
*
|
|
4
|
-
* Validated against herdr 0.
|
|
4
|
+
* Validated against herdr 0.9.1 (protocol 22, Windows named pipe; see WIRE.md). The request shapes
|
|
5
|
+
* pier builds are pinned to herdr's own `api schema` by test/herdr-contract.test.ts — regenerate
|
|
6
|
+
* test/fixtures/herdr-contract.json and bump HERDR_PROTOCOL_EXPECTED when the server moves on:
|
|
5
7
|
* - Transport is NDJSON: request {id, method, params} → response {id, result} | {id, error:{code,message}};
|
|
6
8
|
* - **Control requests use one connection per request**; the server closes it after replying;
|
|
7
9
|
* - Windows targets are named pipes whose name is the complete socket_path with a \\.\pipe\ prefix;
|
|
8
10
|
* - pane.report_agent reports {pane_id, source, agent, state, message?, agent_session_id?, agent_session_path?}
|
|
9
11
|
* where state ∈ idle|working|blocked|unknown ('done' is server-derived and is not reported by the client);
|
|
10
|
-
* - pane.report_metadata reports {pane_id, source, title?, state_labels?, clear_title?, clear_state_labels
|
|
11
|
-
*
|
|
12
|
+
* - pane.report_metadata reports {pane_id, source, title?, state_labels?, clear_title?, clear_state_labels?,
|
|
13
|
+
* tokens?, ttl_ms?} (ttl_ms is capped at 24 h by the server); the first report of a session also clears
|
|
14
|
+
* legacy pi-herdr chunk tokens to null;
|
|
12
15
|
* - agent.list {} → {type:'agent_list', agents: AgentInfo[]};
|
|
13
16
|
* - agent.wait {target, until[], timeout_ms?} → the matching agent, or an error on timeout;
|
|
14
17
|
* - agent.send_keys {target, keys[]} / pane.send_text {pane_id, text}.
|
|
@@ -49,8 +52,54 @@ export interface AgentInfo {
|
|
|
49
52
|
session: string | null;
|
|
50
53
|
stateLabels: Record<string, string>;
|
|
51
54
|
tokens: Record<string, string | null>;
|
|
55
|
+
/** Herdr 0.9.1: foreground cwd of the process currently controlling the PTY. */
|
|
56
|
+
foregroundCwd?: string | null;
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
export interface PaneListItem {
|
|
60
|
+
paneId: string;
|
|
61
|
+
tabId: string;
|
|
62
|
+
workspaceId: string;
|
|
63
|
+
agentStatus: string;
|
|
64
|
+
foregroundCwd?: string | null;
|
|
65
|
+
terminalTitle?: string | null;
|
|
66
|
+
terminalTitleStripped?: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PaneLayoutCell {
|
|
70
|
+
paneId: string;
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
w: number;
|
|
74
|
+
h: number;
|
|
75
|
+
focused: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface PaneLayoutSnapshot {
|
|
79
|
+
tabId: string | null;
|
|
80
|
+
zoomed: boolean;
|
|
81
|
+
focusedPaneId: string | null;
|
|
82
|
+
panes: PaneLayoutCell[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface OpenPluginPaneOptions {
|
|
86
|
+
pluginId: string;
|
|
87
|
+
entrypoint: string;
|
|
88
|
+
placement?: 'popup' | 'overlay' | 'split' | 'tab' | 'zoomed';
|
|
89
|
+
width?: string;
|
|
90
|
+
height?: string;
|
|
91
|
+
focus?: boolean;
|
|
92
|
+
cwd?: string;
|
|
93
|
+
env?: Record<string, string>;
|
|
94
|
+
targetPaneId?: string;
|
|
95
|
+
workspaceId?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type OpenPluginPaneResult =
|
|
99
|
+
| { mode: 'popup'; ok: true }
|
|
100
|
+
| { mode: 'pane'; paneId: string }
|
|
101
|
+
| { mode: 'fallback_tab'; tabId?: string; paneId?: string };
|
|
102
|
+
|
|
54
103
|
/** TabInfo projection; fields mirror the observed tab schema. */
|
|
55
104
|
export interface TabInfo {
|
|
56
105
|
tabId: string;
|
|
@@ -79,12 +128,31 @@ export function herdrSocketTarget(socketPath: string, platform: NodeJS.Platform
|
|
|
79
128
|
return socketPath.startsWith('\\\\.\\pipe\\') ? socketPath : `\\\\.\\pipe\\${socketPath}`;
|
|
80
129
|
}
|
|
81
130
|
|
|
131
|
+
/**
|
|
132
|
+
* B5: herdr can be configured but unreachable (server stopped, stale/typo'd HERDR_SOCKET_PATH).
|
|
133
|
+
* A raw errno ("connect ENOENT /Users/…/herdr.sock") tells the model nothing to act on, so tool
|
|
134
|
+
* layers append this one sentence. Returns null when the failure is not a transport failure.
|
|
135
|
+
*/
|
|
136
|
+
export function herdrUnavailableHint(err: unknown): string | null {
|
|
137
|
+
const msg = String((err as { message?: unknown })?.message ?? err);
|
|
138
|
+
if (!/ENOENT|ECONNREFUSED|ECONNRESET|EPIPE|ENOTCONN|not connected|socket|connect |timeout/i.test(msg)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return `herdr unreachable (${msg}) — check that the herdr server is running and HERDR_SOCKET_PATH matches its socket`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/** Wire protocol this client is written against (herdr 0.9.1). See test/fixtures/herdr-contract.json. */
|
|
146
|
+
export const HERDR_PROTOCOL_EXPECTED = 22;
|
|
147
|
+
|
|
148
|
+
export type WaitForOutputResult =
|
|
149
|
+
| { matched: true }
|
|
150
|
+
| { matched: false; reason: 'timeout' | 'unavailable' };
|
|
82
151
|
|
|
83
152
|
export interface HerdrClientLike {
|
|
84
153
|
readonly available: boolean;
|
|
85
154
|
reportAgent(state: PaneAgentState, message: string | null): Promise<void>;
|
|
86
|
-
|
|
87
|
-
reportMetadata(meta: { session: string; items: TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
155
|
+
reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void>;
|
|
88
156
|
/** M18: report write-lock tokens (lock-<hash> → paneId|path); null releases, and batches stay within 16 keys. */
|
|
89
157
|
reportLockTokens(tokens: Record<string, string | null>): Promise<void>;
|
|
90
158
|
/**
|
|
@@ -109,21 +177,31 @@ export interface HerdrClientLike {
|
|
|
109
177
|
/** v1.2: Focus a pane before adding it to a group tab. */
|
|
110
178
|
focusPane(paneId: string): Promise<void>;
|
|
111
179
|
/** v1.2: Split a new shell pane in the current tab after focus, placing it in the group tab; return its pane ID. */
|
|
112
|
-
splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string }): Promise<string>;
|
|
180
|
+
splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number }): Promise<string>;
|
|
113
181
|
/** v1.2: Close a pane; observed behavior kills its process tree and herdr closes an empty tab. */
|
|
114
182
|
closePane(paneId: string): Promise<void>;
|
|
115
183
|
/** v1.2: Create a tab with a root shell pane, returning tabId/paneId for group-tab infrastructure. */
|
|
116
184
|
createTab(opts: { workspaceId: string; label?: string; cwd?: string; env?: Record<string, string> }): Promise<{ tabId: string; paneId: string }>;
|
|
117
185
|
/** v1.2: List all panes with tab ownership for group-tab additions. */
|
|
118
|
-
listPanes(): Promise<
|
|
186
|
+
listPanes(): Promise<PaneListItem[]>;
|
|
119
187
|
/** D91: Export the tab layout tree, locating it by paneId or tabId; best effort returns null on failure. */
|
|
120
|
-
exportLayout(opts
|
|
188
|
+
exportLayout(opts?: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null>;
|
|
189
|
+
/** 0.9.1: live pane rects for the tab containing paneId. Best effort returns null. */
|
|
190
|
+
paneLayout(opts?: { paneId?: string }): Promise<PaneLayoutSnapshot | null>;
|
|
121
191
|
/** v1.3: List tabs (tab.list); fields follow the observed schema. */
|
|
122
192
|
tabList(): Promise<TabInfo[]>;
|
|
123
193
|
/** v1.3: Get tab details (tab.get); return null when absent. */
|
|
124
194
|
tabGet(tabId: string): Promise<TabInfo | null>;
|
|
125
195
|
/** v1.3: Close a tab (tab.close), cascading to its panes. */
|
|
126
196
|
tabClose(tabId: string): Promise<void>;
|
|
197
|
+
/** 0.9.1: Launch a manifest pane entrypoint with placement (popup/tab/split/overlay/zoomed). */
|
|
198
|
+
openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult>;
|
|
199
|
+
/** 0.9.1: Close active popup if open; returns false if no popup was open. */
|
|
200
|
+
closePopup(): Promise<boolean>;
|
|
201
|
+
/** 0.9.1: Query agent explain diagnostics. */
|
|
202
|
+
agentExplain(target: string): Promise<Record<string, unknown> | null>;
|
|
203
|
+
/** 0.9.1: Query server version via ping. Cached in memory. */
|
|
204
|
+
getServerVersion(): Promise<string | null>;
|
|
127
205
|
/** M14: Send a key combination (pane.send_keys; Herdr key combos include ctrl+c, enter, and esc). */
|
|
128
206
|
sendPaneKeys(paneId: string, keys: string[]): Promise<void>;
|
|
129
207
|
/** M14: Read the pane output buffer; recent preserves ANSI data for T6 detection by default. */
|
|
@@ -132,8 +210,14 @@ export interface HerdrClientLike {
|
|
|
132
210
|
lines?: number;
|
|
133
211
|
stripAnsi?: boolean;
|
|
134
212
|
}): Promise<{ text: string; revision: number; truncated: boolean }>;
|
|
135
|
-
/**
|
|
136
|
-
|
|
213
|
+
/** Read the agent output buffer via herdr 0.9 agent.read RPC. */
|
|
214
|
+
readAgent(target: string, opts?: {
|
|
215
|
+
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
216
|
+
lines?: number;
|
|
217
|
+
stripAnsi?: boolean;
|
|
218
|
+
}): Promise<{ text: string; revision: number; truncated: boolean }>;
|
|
219
|
+
/** M14: Wait for output to match a substring or regex; return discriminated result on match/timeout/unavailable. */
|
|
220
|
+
waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult>;
|
|
137
221
|
close(): void;
|
|
138
222
|
}
|
|
139
223
|
|
|
@@ -156,7 +240,6 @@ function findIdIn(obj: unknown, key: string, depth = 0): string | null {
|
|
|
156
240
|
export class NoopHerdrClient implements HerdrClientLike {
|
|
157
241
|
readonly available = false;
|
|
158
242
|
async reportAgent(): Promise<void> {}
|
|
159
|
-
async reportAgentSession(): Promise<void> {}
|
|
160
243
|
async reportMetadata(): Promise<void> {}
|
|
161
244
|
async reportLockTokens(): Promise<void> {}
|
|
162
245
|
async reportDisplayAgent(): Promise<void> {}
|
|
@@ -182,12 +265,15 @@ export class NoopHerdrClient implements HerdrClientLike {
|
|
|
182
265
|
async createTab(): Promise<{ tabId: string; paneId: string }> {
|
|
183
266
|
throw new Error('subagent requires a herdr-managed pane');
|
|
184
267
|
}
|
|
185
|
-
async listPanes(): Promise<
|
|
268
|
+
async listPanes(): Promise<PaneListItem[]> {
|
|
186
269
|
return [];
|
|
187
270
|
}
|
|
188
271
|
async exportLayout(): Promise<null> {
|
|
189
272
|
return null;
|
|
190
273
|
}
|
|
274
|
+
async paneLayout(): Promise<null> {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
191
277
|
async tabList(): Promise<TabInfo[]> {
|
|
192
278
|
return [];
|
|
193
279
|
}
|
|
@@ -195,12 +281,27 @@ export class NoopHerdrClient implements HerdrClientLike {
|
|
|
195
281
|
return null;
|
|
196
282
|
}
|
|
197
283
|
async tabClose(): Promise<void> {}
|
|
284
|
+
async openPluginPane(): Promise<OpenPluginPaneResult> {
|
|
285
|
+
throw new Error('plugin pane requires a herdr-managed pane');
|
|
286
|
+
}
|
|
287
|
+
async closePopup(): Promise<boolean> {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
async agentExplain(): Promise<null> {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
async getServerVersion(): Promise<null> {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
198
296
|
async sendPaneKeys(): Promise<void> {}
|
|
199
297
|
async readPane(): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
200
298
|
return { text: '', revision: 0, truncated: false };
|
|
201
299
|
}
|
|
202
|
-
async
|
|
203
|
-
return
|
|
300
|
+
async readAgent(): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
301
|
+
return { text: '', revision: 0, truncated: false };
|
|
302
|
+
}
|
|
303
|
+
async waitForOutput(): Promise<WaitForOutputResult> {
|
|
304
|
+
return { matched: false, reason: 'unavailable' };
|
|
204
305
|
}
|
|
205
306
|
close(): void {}
|
|
206
307
|
}
|
|
@@ -296,21 +397,11 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
296
397
|
}
|
|
297
398
|
}
|
|
298
399
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await this.request('pane.report_agent_session', {
|
|
303
|
-
pane_id: this.env.paneId,
|
|
304
|
-
source: REPORT_AGENT_SOURCE,
|
|
305
|
-
agent: 'pi',
|
|
306
|
-
agent_session_path: sessionPath,
|
|
307
|
-
});
|
|
308
|
-
} catch {
|
|
309
|
-
/* Silent best effort. */
|
|
310
|
-
}
|
|
311
|
-
}
|
|
400
|
+
// D-2: no reportAgentSession(). herdr's native pi integration owns the session path; pier stopped
|
|
401
|
+
// sending pane.report_agent_session so the field has a single writer. `report_agent` keeps carrying the
|
|
402
|
+
// activity badge, because `state` is a required field there and pier's todo/role text is pier-only data.
|
|
312
403
|
|
|
313
|
-
async reportMetadata(meta: { session: string; items: TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
404
|
+
async reportMetadata(meta: { session: string; items: readonly TodoItem[]; progressSuffix?: string | null; lastWriteAt?: number | null }): Promise<void> {
|
|
314
405
|
try {
|
|
315
406
|
void meta.session;
|
|
316
407
|
const title = formatPaneTitle(meta.items, null, {
|
|
@@ -407,6 +498,7 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
407
498
|
session: session?.value != null ? String(session.value) : null,
|
|
408
499
|
stateLabels: (a.state_labels ?? {}) as Record<string, string>,
|
|
409
500
|
tokens: (a.tokens ?? {}) as Record<string, string | null>,
|
|
501
|
+
...(a.foreground_cwd ? { foregroundCwd: String(a.foreground_cwd) } : {}),
|
|
410
502
|
};
|
|
411
503
|
});
|
|
412
504
|
}
|
|
@@ -438,6 +530,10 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
438
530
|
} catch (err) {
|
|
439
531
|
const msg = String((err as Error)?.message ?? err);
|
|
440
532
|
if (/timeout/i.test(msg)) return null;
|
|
533
|
+
// A14: herdr reports agent_not_found while a freshly spawned pane's agent is still registering;
|
|
534
|
+
// callers poll `waitAgent`, so map it to the same "nothing yet" result as a timeout instead of
|
|
535
|
+
// failing the whole spawn with a raw server error.
|
|
536
|
+
if (/agent_not_found|agent target .* not found/i.test(msg)) return null;
|
|
441
537
|
throw err;
|
|
442
538
|
}
|
|
443
539
|
}
|
|
@@ -452,13 +548,28 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
452
548
|
await this.request('pane.focus', { pane_id: paneId });
|
|
453
549
|
}
|
|
454
550
|
|
|
455
|
-
async splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string } = {}): Promise<string> {
|
|
456
|
-
const
|
|
551
|
+
async splitPane(opts: { direction?: 'left' | 'right' | 'up' | 'down'; cwd?: string; env?: Record<string, string>; targetPaneId?: string; focus?: boolean; ratio?: number } = {}): Promise<string> {
|
|
552
|
+
const params: Record<string, unknown> = {
|
|
457
553
|
direction: opts.direction ?? 'right',
|
|
458
554
|
...(opts.targetPaneId ? { target_pane_id: opts.targetPaneId } : {}),
|
|
459
555
|
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
460
556
|
...(opts.env ? { env: opts.env } : {}),
|
|
461
|
-
|
|
557
|
+
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
558
|
+
...(opts.ratio !== undefined ? { ratio: opts.ratio } : {}),
|
|
559
|
+
};
|
|
560
|
+
try {
|
|
561
|
+
return await this.splitPaneRequest(params);
|
|
562
|
+
} catch (err) {
|
|
563
|
+
if (opts.ratio === undefined) throw err;
|
|
564
|
+
const msg = String((err as Error)?.message ?? err);
|
|
565
|
+
if (!/unknown|invalid_params|unexpected/i.test(msg)) throw err;
|
|
566
|
+
const { ratio: _ratio, ...rest } = params;
|
|
567
|
+
return await this.splitPaneRequest(rest);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private async splitPaneRequest(params: Record<string, unknown>): Promise<string> {
|
|
572
|
+
const result = (await this.request('pane.split', params)) as Record<string, unknown>;
|
|
462
573
|
const paneId = findIdIn(result, 'pane_id');
|
|
463
574
|
if (!paneId) throw new Error('pane.split: no pane_id in response');
|
|
464
575
|
return paneId;
|
|
@@ -481,17 +592,20 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
481
592
|
return { tabId, paneId: paneId ?? '' };
|
|
482
593
|
}
|
|
483
594
|
|
|
484
|
-
async listPanes(): Promise<
|
|
595
|
+
async listPanes(): Promise<PaneListItem[]> {
|
|
485
596
|
const result = (await this.request('pane.list', {})) as { panes?: Array<Record<string, unknown>> } | null;
|
|
486
597
|
return (result?.panes ?? []).map((p) => ({
|
|
487
598
|
paneId: String(p.pane_id ?? ''),
|
|
488
599
|
tabId: String(p.tab_id ?? ''),
|
|
489
600
|
workspaceId: String(p.workspace_id ?? ''),
|
|
490
601
|
agentStatus: String(p.agent_status ?? 'unknown'),
|
|
602
|
+
...(p.foreground_cwd ? { foregroundCwd: String(p.foreground_cwd) } : {}),
|
|
603
|
+
...(p.terminal_title ? { terminalTitle: String(p.terminal_title) } : {}),
|
|
604
|
+
...(p.terminal_title_stripped ? { terminalTitleStripped: String(p.terminal_title_stripped) } : {}),
|
|
491
605
|
}));
|
|
492
606
|
}
|
|
493
607
|
|
|
494
|
-
async exportLayout(opts: { paneId?: string; tabId?: string }): Promise<{ tabId: string | null; zoomed: boolean; root: unknown } | null> {
|
|
608
|
+
async exportLayout(opts: { paneId?: string; tabId?: string } = {}): Promise<{ tabId: string | null; zoomed: boolean; root: unknown; focusedPaneId: string | null } | null> {
|
|
495
609
|
try {
|
|
496
610
|
const result = (await this.request('layout.export', {
|
|
497
611
|
...(opts.paneId ? { pane_id: opts.paneId } : {}),
|
|
@@ -504,6 +618,42 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
504
618
|
tabId: typeof layout.tab_id === 'string' ? layout.tab_id : null,
|
|
505
619
|
zoomed: Boolean(layout.zoomed),
|
|
506
620
|
root: layout.root ?? null,
|
|
621
|
+
// D-4: focus heat needs to know which pane the human is on; layout.export reports it.
|
|
622
|
+
focusedPaneId: typeof layout.focused_pane_id === 'string' ? layout.focused_pane_id : null,
|
|
623
|
+
};
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async paneLayout(opts: { paneId?: string } = {}): Promise<PaneLayoutSnapshot | null> {
|
|
630
|
+
try {
|
|
631
|
+
const result = (await this.request('pane.layout', {
|
|
632
|
+
...(opts.paneId ? { pane_id: opts.paneId } : {}),
|
|
633
|
+
})) as Record<string, unknown> | null;
|
|
634
|
+
const layout = (result?.layout ?? result) as Record<string, unknown> | null | undefined;
|
|
635
|
+
if (!layout || typeof layout !== 'object' || !Array.isArray(layout.panes)) return null;
|
|
636
|
+
const panes: PaneLayoutCell[] = [];
|
|
637
|
+
for (const raw of layout.panes) {
|
|
638
|
+
if (!raw || typeof raw !== 'object') continue;
|
|
639
|
+
const p = raw as Record<string, unknown>;
|
|
640
|
+
const rect = (p.rect && typeof p.rect === 'object') ? p.rect as Record<string, unknown> : p;
|
|
641
|
+
const paneId = typeof p.pane_id === 'string' ? p.pane_id : null;
|
|
642
|
+
if (!paneId) continue;
|
|
643
|
+
panes.push({
|
|
644
|
+
paneId,
|
|
645
|
+
x: Number(rect.x) || 0,
|
|
646
|
+
y: Number(rect.y) || 0,
|
|
647
|
+
w: Number(rect.width ?? rect.w) || 0,
|
|
648
|
+
h: Number(rect.height ?? rect.h) || 0,
|
|
649
|
+
focused: p.focused === true,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
tabId: typeof layout.tab_id === 'string' ? layout.tab_id : null,
|
|
654
|
+
zoomed: Boolean(layout.zoomed),
|
|
655
|
+
focusedPaneId: typeof layout.focused_pane_id === 'string' ? layout.focused_pane_id : null,
|
|
656
|
+
panes,
|
|
507
657
|
};
|
|
508
658
|
} catch {
|
|
509
659
|
return null;
|
|
@@ -535,6 +685,89 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
535
685
|
await this.request('tab.close', { tab_id: tabId });
|
|
536
686
|
}
|
|
537
687
|
|
|
688
|
+
async openPluginPane(opts: OpenPluginPaneOptions): Promise<OpenPluginPaneResult> {
|
|
689
|
+
const params: Record<string, unknown> = {
|
|
690
|
+
plugin_id: opts.pluginId,
|
|
691
|
+
entrypoint: opts.entrypoint,
|
|
692
|
+
...(opts.placement ? { placement: opts.placement } : {}),
|
|
693
|
+
...(opts.width ? { width: opts.width } : {}),
|
|
694
|
+
...(opts.height ? { height: opts.height } : {}),
|
|
695
|
+
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
696
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
697
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
698
|
+
...(opts.targetPaneId ? { target_pane_id: opts.targetPaneId } : {}),
|
|
699
|
+
...(opts.workspaceId ? { workspace_id: opts.workspaceId } : {}),
|
|
700
|
+
};
|
|
701
|
+
try {
|
|
702
|
+
const result = (await this.request('plugin.pane.open', params)) as Record<string, unknown> | null;
|
|
703
|
+
if (opts.placement === 'popup') {
|
|
704
|
+
return { mode: 'popup', ok: true };
|
|
705
|
+
}
|
|
706
|
+
return { mode: 'pane', paneId: findIdIn(result, 'pane_id') ?? '' };
|
|
707
|
+
} catch (err) {
|
|
708
|
+
// 0.9.0 serde rejects unknown `popup` as "unknown variant" / invalid_params — not a dedicated
|
|
709
|
+
// invalid_placement code. Any popup failure retries as tab; tab failure surfaces to Level 3.
|
|
710
|
+
if (opts.placement !== 'popup') throw err;
|
|
711
|
+
const fallbackRes = (await this.request('plugin.pane.open', {
|
|
712
|
+
plugin_id: opts.pluginId,
|
|
713
|
+
entrypoint: opts.entrypoint,
|
|
714
|
+
placement: 'tab',
|
|
715
|
+
...(opts.focus !== undefined ? { focus: opts.focus } : {}),
|
|
716
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
717
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
718
|
+
...(opts.workspaceId ? { workspace_id: opts.workspaceId } : {}),
|
|
719
|
+
})) as Record<string, unknown> | null;
|
|
720
|
+
return {
|
|
721
|
+
mode: 'fallback_tab',
|
|
722
|
+
paneId: findIdIn(fallbackRes, 'pane_id') ?? undefined,
|
|
723
|
+
tabId: findIdIn(fallbackRes, 'tab_id') ?? undefined,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async closePopup(): Promise<boolean> {
|
|
729
|
+
try {
|
|
730
|
+
await this.request('popup.close', {});
|
|
731
|
+
return true;
|
|
732
|
+
} catch (err) {
|
|
733
|
+
const msg = String((err as Error)?.message ?? err);
|
|
734
|
+
if (/popup_not_open|not found|not supported|unknown method/i.test(msg)) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
throw err;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async agentExplain(target: string): Promise<Record<string, unknown> | null> {
|
|
742
|
+
try {
|
|
743
|
+
const result = (await this.request('agent.explain', { target })) as Record<string, unknown> | null;
|
|
744
|
+
if (!result || typeof result !== 'object') return null;
|
|
745
|
+
const nested = result.explain;
|
|
746
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
747
|
+
return nested as Record<string, unknown>;
|
|
748
|
+
}
|
|
749
|
+
return result;
|
|
750
|
+
} catch {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
private cachedVersion: string | null = null;
|
|
756
|
+
|
|
757
|
+
async getServerVersion(): Promise<string | null> {
|
|
758
|
+
if (this.cachedVersion) return this.cachedVersion;
|
|
759
|
+
try {
|
|
760
|
+
const result = (await this.request('ping', {})) as { version?: unknown } | null;
|
|
761
|
+
if (typeof result?.version === 'string') {
|
|
762
|
+
this.cachedVersion = result.version;
|
|
763
|
+
return result.version;
|
|
764
|
+
}
|
|
765
|
+
} catch {
|
|
766
|
+
/* ignore */
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
538
771
|
async sendPaneKeys(paneId: string, keys: string[]): Promise<void> {
|
|
539
772
|
await this.request('pane.send_keys', { pane_id: paneId, keys });
|
|
540
773
|
}
|
|
@@ -560,7 +793,27 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
560
793
|
};
|
|
561
794
|
}
|
|
562
795
|
|
|
563
|
-
async
|
|
796
|
+
async readAgent(target: string, opts: {
|
|
797
|
+
source?: 'visible' | 'recent' | 'recent_unwrapped';
|
|
798
|
+
lines?: number;
|
|
799
|
+
stripAnsi?: boolean;
|
|
800
|
+
} = {}): Promise<{ text: string; revision: number; truncated: boolean }> {
|
|
801
|
+
const result = (await this.request('agent.read', {
|
|
802
|
+
target,
|
|
803
|
+
source: opts.source ?? 'recent',
|
|
804
|
+
format: 'text',
|
|
805
|
+
strip_ansi: opts.stripAnsi ?? true,
|
|
806
|
+
...(opts.lines != null ? { lines: opts.lines } : {}),
|
|
807
|
+
})) as { read?: { text?: unknown; revision?: unknown; truncated?: unknown }; text?: unknown; revision?: unknown; truncated?: unknown } | null;
|
|
808
|
+
const payload = result?.read ?? result ?? {};
|
|
809
|
+
return {
|
|
810
|
+
text: typeof payload.text === 'string' ? payload.text : '',
|
|
811
|
+
revision: typeof payload.revision === 'number' ? payload.revision : 0,
|
|
812
|
+
truncated: payload.truncated === true,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async waitForOutput(paneId: string, match: { type: 'substring' | 'regex'; value: string }, timeoutMs: number): Promise<WaitForOutputResult> {
|
|
564
817
|
try {
|
|
565
818
|
await this.request('pane.wait_for_output', {
|
|
566
819
|
pane_id: paneId,
|
|
@@ -568,10 +821,13 @@ export class HerdrClient implements HerdrClientLike {
|
|
|
568
821
|
match,
|
|
569
822
|
timeout_ms: timeoutMs,
|
|
570
823
|
}, timeoutMs + 5000);
|
|
571
|
-
return true;
|
|
824
|
+
return { matched: true };
|
|
572
825
|
} catch (err) {
|
|
573
826
|
const msg = String((err as Error)?.message ?? err);
|
|
574
|
-
if (/timeout/i.test(msg)) return
|
|
827
|
+
if (/timeout/i.test(msg)) return { matched: false, reason: 'timeout' };
|
|
828
|
+
if (/not supported|not implemented|unknown method|unavailable|method not found/i.test(msg)) {
|
|
829
|
+
return { matched: false, reason: 'unavailable' };
|
|
830
|
+
}
|
|
575
831
|
throw err;
|
|
576
832
|
}
|
|
577
833
|
}
|
package/src/index-locks.ts
CHANGED
|
@@ -16,6 +16,10 @@ import {
|
|
|
16
16
|
} from './lock-core.ts';
|
|
17
17
|
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
18
18
|
|
|
19
|
+
export interface WriteLocksHandle {
|
|
20
|
+
getHeldLocks: () => readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
export function installWriteLocks(
|
|
20
24
|
pi: ExtensionAPI,
|
|
21
25
|
opts: {
|
|
@@ -23,11 +27,15 @@ export function installWriteLocks(
|
|
|
23
27
|
env: HerdrEnv | null;
|
|
24
28
|
hard: boolean;
|
|
25
29
|
},
|
|
26
|
-
):
|
|
30
|
+
): WriteLocksHandle {
|
|
27
31
|
const { client, env, hard } = opts;
|
|
28
32
|
const heldLocks = new Set<string>();
|
|
29
33
|
const lockWarnByToolCall = new Map<string, string>();
|
|
30
34
|
|
|
35
|
+
if (!client.available || !env) {
|
|
36
|
+
return { getHeldLocks: () => [] };
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
async function acquireLocks(paths: readonly string[]): Promise<void> {
|
|
32
40
|
for (const p of paths) heldLocks.add(p);
|
|
33
41
|
await client.reportLockTokens(acquireTokensFor(paths, env?.paneId ?? ''));
|
|
@@ -37,17 +45,24 @@ export function installWriteLocks(
|
|
|
37
45
|
if (!client.available || !env) return;
|
|
38
46
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
39
47
|
let agents: LockAgentView[];
|
|
48
|
+
let ownForegroundCwd: string | null = null;
|
|
40
49
|
try {
|
|
41
|
-
|
|
50
|
+
const liveAgents = await client.listAgents();
|
|
51
|
+
agents = liveAgents.map((a) => ({ paneId: a.paneId, tokens: a.tokens }));
|
|
52
|
+
const me = liveAgents.find((a) => a.paneId === env.paneId);
|
|
53
|
+
if (me?.foregroundCwd) {
|
|
54
|
+
ownForegroundCwd = me.foregroundCwd;
|
|
55
|
+
}
|
|
42
56
|
} catch {
|
|
43
57
|
return;
|
|
44
58
|
}
|
|
59
|
+
const effectiveCwd = ownForegroundCwd ?? cwd;
|
|
45
60
|
const plan = planWriteGuard({
|
|
46
61
|
toolName: event.toolName ?? '',
|
|
47
62
|
input: event.input,
|
|
48
63
|
agents,
|
|
49
64
|
ownPaneId: env.paneId,
|
|
50
|
-
cwd,
|
|
65
|
+
cwd: effectiveCwd,
|
|
51
66
|
hard,
|
|
52
67
|
});
|
|
53
68
|
if (plan.kind === 'skip') return;
|
|
@@ -60,13 +75,14 @@ export function installWriteLocks(
|
|
|
60
75
|
await acquireLocks(plan.paths);
|
|
61
76
|
});
|
|
62
77
|
|
|
63
|
-
pi.on('tool_result', async (event:
|
|
64
|
-
|
|
65
|
-
|
|
78
|
+
pi.on('tool_result', async (event: unknown) => {
|
|
79
|
+
const rec = (event ?? {}) as { toolCallId?: unknown; content?: unknown };
|
|
80
|
+
if (typeof rec.toolCallId !== 'string') return;
|
|
81
|
+
const warning = lockWarnByToolCall.get(rec.toolCallId);
|
|
66
82
|
if (!warning) return;
|
|
67
|
-
lockWarnByToolCall.delete(
|
|
68
|
-
const content = Array.isArray(
|
|
69
|
-
return { content: [...content, { type: 'text', text: warning }] };
|
|
83
|
+
lockWarnByToolCall.delete(rec.toolCallId);
|
|
84
|
+
const content = Array.isArray(rec.content) ? rec.content as Array<{ type: 'text'; text: string }> : [];
|
|
85
|
+
return { content: [...content, { type: 'text' as const, text: warning }] };
|
|
70
86
|
});
|
|
71
87
|
|
|
72
88
|
pi.on('agent_settled', async () => {
|
|
@@ -102,4 +118,6 @@ export function installWriteLocks(
|
|
|
102
118
|
ui?.notify?.(lines.join('\n'), 'info');
|
|
103
119
|
},
|
|
104
120
|
});
|
|
121
|
+
|
|
122
|
+
return { getHeldLocks: () => [...heldLocks] };
|
|
105
123
|
}
|
package/src/index-master.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface MasterPluginMount {
|
|
|
35
35
|
reconcileOnSettlement: (description: string, outcome: 'settled' | 'failed') => string[];
|
|
36
36
|
withReconcileNotes: (base: string, notes: readonly string[]) => string;
|
|
37
37
|
claimSettleNotice: (key: string) => boolean;
|
|
38
|
+
isCompactionInFlight?: () => boolean;
|
|
39
|
+
isIntentionalAbort?: () => boolean;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
async function loadEntry(
|
|
@@ -89,9 +91,12 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
89
91
|
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
90
92
|
},
|
|
91
93
|
state: m.todoUi,
|
|
94
|
+
getBlockedDepth: m.getBlockedDepth,
|
|
92
95
|
stopReminder: {
|
|
93
96
|
getBlockedDepth: m.getBlockedDepth,
|
|
94
97
|
getRunningSubs: () => m.port.current?.listRunningSubs().length ?? 0,
|
|
98
|
+
isCompactionInFlight: m.isCompactionInFlight,
|
|
99
|
+
isIntentionalAbort: m.isIntentionalAbort,
|
|
95
100
|
},
|
|
96
101
|
});
|
|
97
102
|
await loadEntry(sessionRoot, useLoader, './core/todo.ts', todoPlugin);
|
package/src/index-notices.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* tool calls — a long master run queues every settlement and floods at the
|
|
6
6
|
* end. Buffer while busy; flush at turn_end (steer) / agent_settled (followUp).
|
|
7
7
|
*/
|
|
8
|
-
import { collapseNotices } from './notice-buffer.ts';
|
|
8
|
+
import { NOTICE_MAX_SHOWN, collapseNotices } from './notice-buffer.ts';
|
|
9
9
|
|
|
10
10
|
export type NoticeSendMode = 'steer' | 'followUp';
|
|
11
11
|
|
|
@@ -19,6 +19,12 @@ export function createNoticeBuffer(opts: {
|
|
|
19
19
|
/** True while the agent is in a turn, or after an abort that must not wake a new run. */
|
|
20
20
|
isBusy: () => boolean;
|
|
21
21
|
send: (content: string, mode: NoticeSendMode) => Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* P0-2 (RFC docs/rfc-jev-integration.md §3): relevance reorder of a collapsed
|
|
24
|
+
* batch before truncation. Only consulted above the show cap; returning null
|
|
25
|
+
* (or omitting the hook) keeps arrival order. Implementations must not throw.
|
|
26
|
+
*/
|
|
27
|
+
rank?: (contents: readonly string[]) => Promise<readonly string[] | null>;
|
|
22
28
|
}): NoticeBuffer {
|
|
23
29
|
const pending: string[] = [];
|
|
24
30
|
const pendingPaneIds = new Set<string>();
|
|
@@ -35,8 +41,13 @@ export function createNoticeBuffer(opts: {
|
|
|
35
41
|
noticePending: () => pendingPaneIds,
|
|
36
42
|
async flush(mode) {
|
|
37
43
|
if (pending.length === 0) return;
|
|
38
|
-
const
|
|
44
|
+
const batch = pending.splice(0);
|
|
45
|
+
let ordered: readonly string[] = batch;
|
|
46
|
+
if (opts.rank && batch.length > NOTICE_MAX_SHOWN) {
|
|
47
|
+
ordered = (await opts.rank(batch)) ?? batch;
|
|
48
|
+
}
|
|
39
49
|
pendingPaneIds.clear();
|
|
50
|
+
const collapsed = collapseNotices(ordered);
|
|
40
51
|
if (collapsed) await opts.send(collapsed, mode);
|
|
41
52
|
},
|
|
42
53
|
};
|
package/src/index-pipe.ts
CHANGED
|
@@ -53,6 +53,11 @@ export async function handlePipeRequest(
|
|
|
53
53
|
return { type: 'ok', id: req.id };
|
|
54
54
|
case 'reply': {
|
|
55
55
|
s.port.current?.applyReplySession(req.paneId, req.sessionFile);
|
|
56
|
+
// B8: the claim key is `${paneId}:${requestId}` and the poll loop claims exactly the same shape
|
|
57
|
+
// (subagent-poll-loop.ts settle path). The two agree because a child echoes the id of the pipe
|
|
58
|
+
// request it answered (`id: req.id` in index.ts's settle push), and the parent hands that same
|
|
59
|
+
// id to startPoller (`prompt-<taskId>` / `fu-<ts>` in core/subagent.ts). Keep both sides in step:
|
|
60
|
+
// a mismatch here surfaces as two "finished" notices for one settlement.
|
|
56
61
|
if (s.claimSettleNotice(`${req.paneId}:${req.id}`)) {
|
|
57
62
|
const notes = s.port.current?.reconcileOnReply(req.paneId) ?? [];
|
|
58
63
|
const statLine = await s.port.current?.settleStatLine(req.paneId) ?? null;
|