pi-pier 0.3.1 → 0.4.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.
- package/README.md +8 -5
- package/package.json +3 -3
- package/src/bootstrap.ts +2 -1
- package/src/compact-coordinator.ts +3 -2
- 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 +1 -4
- package/src/efficiency-store.ts +1 -1
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +126 -31
- 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 +14 -0
- package/src/index-runtime.ts +14 -0
- package/src/index-worker.ts +4 -10
- package/src/index.ts +298 -48
- package/src/jev-core.ts +1 -3
- package/src/panel-open.ts +47 -0
- package/src/pier-options.ts +1 -2
- package/src/pipe-channel.ts +17 -0
- package/src/plugins/subagent.ts +68 -30
- package/src/plugins/terminal.ts +34 -15
- package/src/plugins/todo.ts +20 -42
- package/src/reducer-core.ts +3 -5
- package/src/renderers.ts +37 -2
- package/src/role-manifest.ts +2 -3
- package/src/roles/worker-default.json +3 -3
- package/src/settle-wake-core.ts +9 -1
- package/src/subagent-core.ts +170 -2
- package/src/subagent-poller.ts +17 -1
- package/src/subagent-spawn.ts +51 -4
- package/src/terminal-core.ts +68 -34
- package/src/todo-read-hook.ts +23 -0
- package/src/tree-view.ts +162 -0
- package/src/worker-marker.ts +76 -0
- package/src/focus-poller.ts +0 -223
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* details persist in session JSONL and getBranch() replay implements branch rollback.
|
|
11
11
|
*/
|
|
12
12
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
13
|
+
import { Type } from 'typebox';
|
|
13
14
|
import {
|
|
14
15
|
ASK_PARAMETERS,
|
|
15
16
|
ASK_PROMPT_GUIDELINES,
|
|
@@ -28,6 +29,8 @@ import { createHerdrClient } from './herdr-client.ts';
|
|
|
28
29
|
import { bareSessionId, lastAssistantText, readSessionFile, resolveSessionFileValue } from './session-tail.ts';
|
|
29
30
|
import { fileURLToPath } from 'node:url';
|
|
30
31
|
import { join } from 'node:path';
|
|
32
|
+
import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
33
|
+
import { tmpdir } from 'node:os';
|
|
31
34
|
import { platformPaths } from './platform-paths.ts';
|
|
32
35
|
import { TodosService, type TodoCompletionSource } from './todos-service.ts';
|
|
33
36
|
import { reconcileTodos } from './reconcile-core.ts';
|
|
@@ -38,12 +41,23 @@ import { ABORT_STOP_REASON, planSettleWake, shouldPushSettleReply } from './sett
|
|
|
38
41
|
import { parseRuntimeManifest } from './tool-gate.ts';
|
|
39
42
|
import { formatPaneTitle } from './pane-title.ts';
|
|
40
43
|
import { registerSlimFrame, updateSlimFrame } from './slim-frame.ts';
|
|
41
|
-
import { planIndexMode } from './index-runtime.ts';
|
|
42
|
-
import { emptySubagentPortBox } from './subagent-core.ts';
|
|
44
|
+
import { applyRestoredWorkerMode, planIndexMode } from './index-runtime.ts';
|
|
45
|
+
import { buildResumeArgv, emptySubagentPortBox, PROGRESS_CUSTOM_TYPE } from './subagent-core.ts';
|
|
46
|
+
import { hasWorkerMarker, writeWorkerMarker, readWorkerMarker } from './worker-marker.ts';
|
|
47
|
+
import {
|
|
48
|
+
DELEGATION_CUSTOM_TYPE,
|
|
49
|
+
applyWorkerTag,
|
|
50
|
+
buildDelegationSection,
|
|
51
|
+
foldDelegation,
|
|
52
|
+
parseDelegationContext,
|
|
53
|
+
type DelegationContext,
|
|
54
|
+
} from './delegation-core.ts';
|
|
55
|
+
import { formatDelegationTree, type WorkerTodoSnapshot, type WorkerWidgetRow } from './tree-view.ts';
|
|
43
56
|
import { createNoticeBuffer } from './index-notices.ts';
|
|
44
57
|
import { handlePipeRequest, type MachineRequest } from './index-pipe.ts';
|
|
45
58
|
import { installWriteLocks } from './index-locks.ts';
|
|
46
59
|
import { installDashboardCommand } from './dashboard-command.ts';
|
|
60
|
+
import { openPierPanel } from './panel-open.ts';
|
|
47
61
|
import { registerObservationPack, createCompactionBatchPackHook, type PickMiddleExcerpt } from './plugins/observation.ts';
|
|
48
62
|
import { createJevRuntime } from './jev-client.ts';
|
|
49
63
|
import {
|
|
@@ -62,25 +76,15 @@ import { handleReducerToolResult, type ToolResultEventLike } from './reducer-inv
|
|
|
62
76
|
import { appendEfficiencyLog, efficiencyLogPath, pruneSessionObjects, resolveSessionRoot } from './efficiency-store.ts';
|
|
63
77
|
import type { RoutingTelemetryRecord, RoutingTelemetryRow } from './routing-telemetry.ts';
|
|
64
78
|
import { loadEfficiencyConfigFromDisk, resolveEfficiencyConfig, type EfficiencyConfig } from './efficiency-config-core.ts';
|
|
65
|
-
import { resolveDefaultFocusPollMs, parseFocusSample, spawnReflow, startFocusPoller, type FocusPoller } from './focus-poller.ts';
|
|
66
79
|
import { composeMasterRuntime, createRoleRuntime } from './index-roles.ts';
|
|
67
80
|
import { createGateRuntime } from './index-gates.ts';
|
|
68
81
|
|
|
69
|
-
/** D-4: PIER_FOCUS_POLL_MS=0 disables the poller; invalid values fall back to the version default. */
|
|
70
|
-
function focusPollIntervalMs(env: NodeJS.ProcessEnv = process.env, herdrVersion?: string | null): number {
|
|
71
|
-
const defaultMs = resolveDefaultFocusPollMs(herdrVersion);
|
|
72
|
-
const ms = Number(env.PIER_FOCUS_POLL_MS);
|
|
73
|
-
return env.PIER_FOCUS_POLL_MS !== undefined && env.PIER_FOCUS_POLL_MS.trim() !== '' && Number.isFinite(ms) && ms >= 0
|
|
74
|
-
? ms
|
|
75
|
-
: defaultMs;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
82
|
function agentSessionsDir(): string {
|
|
79
83
|
return process.env.PI_CODING_AGENT_DIR ? join(process.env.PI_CODING_AGENT_DIR, 'sessions') : platformPaths.sessionsDir;
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
export default async function (pi: ExtensionAPI) {
|
|
83
|
-
const mode = planIndexMode();
|
|
87
|
+
const mode = applyRestoredWorkerMode(planIndexMode(), process.env.PI_SESSION_FILE ?? null, hasWorkerMarker);
|
|
84
88
|
const isSubagent = mode.isSubagent;
|
|
85
89
|
const roleBase = process.env.PI_HERDR_ROLE_BASE || process.cwd();
|
|
86
90
|
|
|
@@ -89,7 +93,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
89
93
|
let effConfig: EfficiencyConfig = resolveEfficiencyConfig();
|
|
90
94
|
const coordinator = new CompactCoordinator();
|
|
91
95
|
const { client, env } = createHerdrClient();
|
|
92
|
-
|
|
96
|
+
/** RFC rfc-delegation-identity §2.1: resolved env → marker → branch fold; worker panes only. */
|
|
97
|
+
let delegation: DelegationContext | null = parseDelegationContext(process.env.PI_HERDR_DELEGATION);
|
|
93
98
|
const initialManifest =
|
|
94
99
|
parseRuntimeManifest(process.env.PI_HERDR_ROLE_MANIFEST) ??
|
|
95
100
|
(mode.composeMaster ? composeMasterRuntime() : null);
|
|
@@ -189,16 +194,87 @@ export default async function (pi: ExtensionAPI) {
|
|
|
189
194
|
const p = progressOf(todos.items);
|
|
190
195
|
const eta = estimateEta({ completedAt: completedStamps, total: p.total, now: Date.now() });
|
|
191
196
|
const suffix = formatProgressSuffix({ completed: p.completed, total: p.total, eta });
|
|
192
|
-
const title =
|
|
197
|
+
const title = applyWorkerTag(
|
|
198
|
+
formatPaneTitle(todos.items, null, { progressSuffix: suffix, lastWriteAt: todos.lastWriteAt }),
|
|
199
|
+
delegation?.description ?? null,
|
|
200
|
+
);
|
|
193
201
|
updateSlimFrame({ title, items: todos.items, lastWriteAt: todos.lastWriteAt });
|
|
194
202
|
client.reportMetadata({
|
|
195
203
|
session: label,
|
|
196
204
|
items: todos.items,
|
|
197
205
|
progressSuffix: suffix,
|
|
198
206
|
lastWriteAt: todos.lastWriteAt,
|
|
207
|
+
workerTag: delegation?.description ?? null,
|
|
199
208
|
}).catch(() => {});
|
|
209
|
+
// D113: delegated workers also push the full list to the master's pipe so its resident widget
|
|
210
|
+
// and /pier-tree stay live without polling. Best effort — a lost push self-heals on the next write.
|
|
211
|
+
if (isSubagent && delegation?.parentPipe) {
|
|
212
|
+
void pipeRequest(delegation.parentPipe, {
|
|
213
|
+
type: 'todo_sync',
|
|
214
|
+
id: `tsync-${Date.now()}`,
|
|
215
|
+
paneId: env?.paneId ?? '',
|
|
216
|
+
items: todos.items.map((it) => ({ content: it.content, status: it.status, ...(it.blocker ? { blocker: it.blocker } : {}) })),
|
|
217
|
+
summary: title ?? delegation.description,
|
|
218
|
+
}, 3000).catch(() => {});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** D113: per-worker todo cache (pipe pushes; summary fallback after a master resume). */
|
|
223
|
+
const workerTodos = new Map<string, WorkerTodoSnapshot>();
|
|
224
|
+
function applyWorkerTodos(workerPaneId: string, items: WorkerTodoSnapshot['items'], summary: string): void {
|
|
225
|
+
if (!workerPaneId) return;
|
|
226
|
+
workerTodos.set(workerPaneId, { items, summary, ts: Date.now() });
|
|
227
|
+
todoUi.rerenderWidget?.();
|
|
228
|
+
refreshTreeSnapshot();
|
|
229
|
+
}
|
|
230
|
+
/** Running rows joined with their cached todos — feeds the unified widget and /pier-tree. */
|
|
231
|
+
function widgetWorkers(): WorkerWidgetRow[] {
|
|
232
|
+
return (subagentPort.current?.listRunningSubs() ?? []).map((s) => ({
|
|
233
|
+
...s,
|
|
234
|
+
todos: workerTodos.get(s.paneId) ?? null,
|
|
235
|
+
}));
|
|
200
236
|
}
|
|
201
237
|
|
|
238
|
+
/* ── /pier-tree panel: the pi process owns the data, the popup/overlay owns the surface. ── */
|
|
239
|
+
/** Set while a panel is open; todo/worker updates republish the snapshot it polls. */
|
|
240
|
+
let treeSnapshot: { path: string } | null = null;
|
|
241
|
+
|
|
242
|
+
function renderTreeLines(): string[] {
|
|
243
|
+
const rows = subagentPort.current?.listRows() ?? [];
|
|
244
|
+
return formatDelegationTree({
|
|
245
|
+
selfItems: todos.items,
|
|
246
|
+
workers: widgetWorkers(),
|
|
247
|
+
settled: rows
|
|
248
|
+
.filter((r) => r.status === 'settled' || r.status === 'consumed')
|
|
249
|
+
.map((r) => ({ paneId: r.paneId, slug: r.slug, description: r.description, settledAt: r.settledAt })),
|
|
250
|
+
now: Date.now(),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function treeSnapshotPath(): string {
|
|
255
|
+
const id = (sessionId || (env ? `pane-${env.paneId}` : 'session')).replace(/[^A-Za-z0-9._-]/g, '_').slice(0, 80);
|
|
256
|
+
return join(tmpdir(), 'pier-tree', `${id}.txt`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function publishTreeSnapshot(): string {
|
|
260
|
+
const path = treeSnapshotPath();
|
|
261
|
+
mkdirSync(join(tmpdir(), 'pier-tree'), { recursive: true });
|
|
262
|
+
writeFileSync(path, `${renderTreeLines().join('\n')}\n`);
|
|
263
|
+
treeSnapshot = { path };
|
|
264
|
+
return path;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function refreshTreeSnapshot(): void {
|
|
268
|
+
if (!treeSnapshot) return;
|
|
269
|
+
try {
|
|
270
|
+
writeFileSync(treeSnapshot.path, `${renderTreeLines().join('\n')}\n`);
|
|
271
|
+
} catch {
|
|
272
|
+
/* The panel may have been closed and its temp dir swept; the next command republishes. */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
todos.on('todo.updated', () => refreshTreeSnapshot());
|
|
277
|
+
|
|
202
278
|
/** M17 settlement reconciliation: auto-complete settled todos / unblock on failure notes (idempotent). */
|
|
203
279
|
function reconcileOnSettlement(description: string, outcome: 'settled' | 'failed'): string[] {
|
|
204
280
|
if (!description) return [];
|
|
@@ -246,10 +322,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
246
322
|
reportToolBadge();
|
|
247
323
|
}
|
|
248
324
|
});
|
|
249
|
-
pi.on('tool_execution_end', async (event: { toolCallId?: string }) => {
|
|
250
|
-
if (typeof event?.toolCallId
|
|
251
|
-
|
|
252
|
-
|
|
325
|
+
pi.on('tool_execution_end', async (event: { toolCallId?: string; toolName?: string; durationMs?: number }) => {
|
|
326
|
+
if (typeof event?.toolCallId !== 'string') return;
|
|
327
|
+
const tool = typeof event.toolName === 'string' ? event.toolName : runningTools.get(event.toolCallId) ?? null;
|
|
328
|
+
runningTools.delete(event.toolCallId);
|
|
329
|
+
reportToolBadge();
|
|
330
|
+
// pi ≥ 1.1 records the real execution time on the event; the tools ledger replaces wall-clock
|
|
331
|
+
// estimates in the efficiency trial with measured numbers. Absent on older pi — nothing logged.
|
|
332
|
+
if (typeof event.durationMs === 'number' && sessionRoot) {
|
|
333
|
+
try {
|
|
334
|
+
await appendEfficiencyLog(efficiencyLogPath(sessionRoot, 'tools'), {
|
|
335
|
+
sessionId: sessionId || 'unknown',
|
|
336
|
+
ts: Date.now(),
|
|
337
|
+
tool,
|
|
338
|
+
durationMs: event.durationMs,
|
|
339
|
+
});
|
|
340
|
+
} catch {
|
|
341
|
+
/* ledger is observability only */
|
|
342
|
+
}
|
|
253
343
|
}
|
|
254
344
|
});
|
|
255
345
|
|
|
@@ -274,6 +364,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
274
364
|
.sendUserMessage?.(content, { deliverAs: mode, triggerTurn: true });
|
|
275
365
|
return Promise.resolve();
|
|
276
366
|
};
|
|
367
|
+
|
|
368
|
+
/** Phase 2 (RFC rfc-delegation-identity §3.2): a worker's progress note enters master context as
|
|
369
|
+
* a display-less custom message — no turn, no visible chat line; renderers.ts gives it a card. */
|
|
370
|
+
const deliverProgress = (workerPaneId: string, text: string): Promise<void> => {
|
|
371
|
+
(pi as unknown as {
|
|
372
|
+
sendMessage?: (msg: { customType: string; content: string; display: boolean; details?: Record<string, unknown> }, opts?: { triggerTurn?: boolean }) => void;
|
|
373
|
+
}).sendMessage?.(
|
|
374
|
+
{ customType: PROGRESS_CUSTOM_TYPE, content: text.slice(0, 2000), display: false, details: { paneId: workerPaneId } },
|
|
375
|
+
{ triggerTurn: false },
|
|
376
|
+
);
|
|
377
|
+
return Promise.resolve();
|
|
378
|
+
};
|
|
277
379
|
const notices = createNoticeBuffer({
|
|
278
380
|
isBusy: () => agentActive || lastStopReason === ABORT_STOP_REASON,
|
|
279
381
|
send: sendUserMessageAs,
|
|
@@ -341,13 +443,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
341
443
|
abort: () => { latestCtx?.abort?.(); },
|
|
342
444
|
setPendingMachineRequest: (next) => { pendingMachineRequest = next; },
|
|
343
445
|
applyRoleSwitch: roles.applyRoleSwitch,
|
|
446
|
+
deliverProgress,
|
|
447
|
+
applyWorkerTodos,
|
|
344
448
|
}));
|
|
345
449
|
} catch {
|
|
346
450
|
/* Pipe name collision (rare): this session has no channel; callers report after ping timeout. */
|
|
347
451
|
}
|
|
348
452
|
}
|
|
349
453
|
|
|
350
|
-
async function pushSettleReply(): Promise<void> {
|
|
454
|
+
async function pushSettleReply(aborted = false): Promise<void> {
|
|
351
455
|
const req = pendingMachineRequest;
|
|
352
456
|
if (!req || !req.push || !req.from) return;
|
|
353
457
|
// An aborted settle is not a settlement: OCC's abort means the continuation turn is coming, and
|
|
@@ -356,6 +460,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
356
460
|
// request armed; the post-compaction settle pushes it (settle-wake-core.ts).
|
|
357
461
|
if (!shouldPushSettleReply({
|
|
358
462
|
intentionalAbort: coordinator.intentionalAbort,
|
|
463
|
+
aborted,
|
|
359
464
|
compactionInFlight: coordinator.compactionInFlight,
|
|
360
465
|
lastStopReason,
|
|
361
466
|
})) {
|
|
@@ -383,8 +488,6 @@ export default async function (pi: ExtensionAPI) {
|
|
|
383
488
|
}
|
|
384
489
|
}
|
|
385
490
|
|
|
386
|
-
const sessionFocusPoller: { current: FocusPoller | null } = { current: null };
|
|
387
|
-
|
|
388
491
|
pi.on('session_start', async (event, ctx) => {
|
|
389
492
|
sessionId = resolveSessionId(ctx);
|
|
390
493
|
latestCtx = ctx as { abort?: () => void } | null;
|
|
@@ -412,31 +515,57 @@ export default async function (pi: ExtensionAPI) {
|
|
|
412
515
|
// Self-healing: the ask marker lives in herdr with a 24h TTL; a session killed
|
|
413
516
|
// while a dialog was open must not keep the pane looking blocked forever.
|
|
414
517
|
void client.reportAskFlag(null).catch(() => {});
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
518
|
+
if (isSubagent) {
|
|
519
|
+
// herdr 0.9.2 restore identity: the marker re-derives worker mode for the resumed boot
|
|
520
|
+
// (resume_argv cannot carry spawn env), and the self-reported command replays this exact
|
|
521
|
+
// session — extension, role model, approve, fullscreen — instead of a bare `pi --resume`.
|
|
522
|
+
const sm = typeof ctx === 'object' && ctx !== null && 'sessionManager' in ctx ? ctx.sessionManager : undefined;
|
|
523
|
+
const sessionFile = (typeof sm?.getSessionFile === 'function' ? sm.getSessionFile() : undefined)
|
|
524
|
+
?? process.env.PI_SESSION_FILE ?? null;
|
|
525
|
+
// RFC rfc-delegation-identity §2.1: env (spawn) > marker (herdr restart) > branch fold (/resume).
|
|
526
|
+
if (!delegation) delegation = readWorkerMarker(sessionFile)?.delegation ?? null;
|
|
527
|
+
const branchEntries = (ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } })
|
|
528
|
+
?.sessionManager?.getBranch?.() ?? [];
|
|
529
|
+
const folded = foldDelegation(branchEntries as Parameters<typeof foldDelegation>[0]);
|
|
530
|
+
if (!delegation && folded) delegation = folded;
|
|
531
|
+
if (delegation && !folded) {
|
|
532
|
+
// First-frame card: write once — the entry doubles as the /resume replay source.
|
|
533
|
+
try {
|
|
534
|
+
pi.appendEntry(DELEGATION_CUSTOM_TYPE, { version: 1, ...delegation, ts: Date.now() });
|
|
535
|
+
} catch {
|
|
536
|
+
/* Best effort; the marker already carries the identity. */
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (delegation) void client.reportDelegationTokens(delegation).catch(() => {});
|
|
540
|
+
if (sessionRoot) {
|
|
541
|
+
writeWorkerMarker(sessionFile, roles.state.manifest?.role, delegation ?? undefined);
|
|
542
|
+
const resumeArgv = buildResumeArgv(process.argv);
|
|
543
|
+
if (resumeArgv && client.available) {
|
|
544
|
+
void client.reportAgentSessionResume(sessionId, resumeArgv).catch(() => {});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
436
547
|
}
|
|
548
|
+
|
|
549
|
+
// Focus heat is event-driven on herdr 0.9.5+: the workbench receives pane.focused directly,
|
|
550
|
+
// so no per-pane layout sampler is started here.
|
|
437
551
|
startPipeServerFor(cwd);
|
|
438
552
|
});
|
|
439
553
|
|
|
554
|
+
/* RFC rfc-delegation-identity §2.2: worker identity + delivery contract ride the prompt
|
|
555
|
+
* sections each turn (same diff discipline as pier-role; removal cleans up). */
|
|
556
|
+
if (isSubagent) {
|
|
557
|
+
pi.on('before_agent_start', async (event) => {
|
|
558
|
+
const sections = event.systemPromptOptions?.sections;
|
|
559
|
+
if (!sections) return;
|
|
560
|
+
if (delegation) {
|
|
561
|
+
const text = buildDelegationSection(delegation, env?.paneId ?? '');
|
|
562
|
+
if (sections['pier-delegation'] !== text) sections['pier-delegation'] = text;
|
|
563
|
+
} else {
|
|
564
|
+
delete sections['pier-delegation'];
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
440
569
|
pi.on('session_tree', async (_event, ctx) => {
|
|
441
570
|
sessionId = resolveSessionId(ctx);
|
|
442
571
|
rebuildFromBranch(ctx);
|
|
@@ -447,6 +576,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
447
576
|
mirrorTodos();
|
|
448
577
|
});
|
|
449
578
|
|
|
579
|
+
/* pi 1.2 counts the run as busy from `before_agent_start`, not `turn_start`: a settlement
|
|
580
|
+
* notice arriving in that window must enter the D92 buffer instead of being sent immediately. */
|
|
581
|
+
pi.on('before_agent_start', async () => {
|
|
582
|
+
agentActive = true;
|
|
583
|
+
});
|
|
584
|
+
|
|
450
585
|
pi.on('turn_start', async () => {
|
|
451
586
|
agentActive = true;
|
|
452
587
|
// A11: reset so a previous turn's abort cannot taint this one.
|
|
@@ -493,12 +628,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
493
628
|
void notices.flush('steer');
|
|
494
629
|
});
|
|
495
630
|
|
|
496
|
-
pi.on('agent_settled', async (
|
|
631
|
+
pi.on('agent_settled', async (event: unknown, ctx) => {
|
|
497
632
|
agentActive = false;
|
|
498
633
|
gates.report('idle', null);
|
|
634
|
+
// pi ≥ 1.1 stamps cancelled runs on the event itself; stopReason tracking misses ESC mid-tool
|
|
635
|
+
// (no final assistant message), so the event field is authoritative when present.
|
|
636
|
+
const aborted = event !== null && typeof event === 'object' && 'aborted' in event && event.aborted === true;
|
|
499
637
|
const plan = planSettleWake({
|
|
500
638
|
lastStopReason,
|
|
501
639
|
intentionalAbort: coordinator.intentionalAbort,
|
|
640
|
+
aborted,
|
|
502
641
|
compactionPending: coordinator.selectedCompaction !== null || coordinator.compactionInFlight,
|
|
503
642
|
running: subagentPort.current?.listRunningSubs() ?? [],
|
|
504
643
|
lastNoticeKey: d96NoticeKey,
|
|
@@ -529,8 +668,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
529
668
|
}),
|
|
530
669
|
});
|
|
531
670
|
}
|
|
532
|
-
if (lastStopReason !== ABORT_STOP_REASON) void notices.flush('followUp');
|
|
533
|
-
void pushSettleReply();
|
|
671
|
+
if (lastStopReason !== ABORT_STOP_REASON && !aborted) void notices.flush('followUp');
|
|
672
|
+
void pushSettleReply(aborted);
|
|
534
673
|
});
|
|
535
674
|
|
|
536
675
|
/* ── D101 ObservationPack (obs_recall tool & context projector) ── */
|
|
@@ -552,6 +691,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
552
691
|
env,
|
|
553
692
|
getTodoItems: () => todos.items,
|
|
554
693
|
getHeldLocks: () => locksHandle.getHeldLocks(),
|
|
694
|
+
prepareTree: () => publishTreeSnapshot(),
|
|
555
695
|
});
|
|
556
696
|
|
|
557
697
|
/* ── ask_user_question (v1.3 M8 human gate; master and subagents) ── */
|
|
@@ -577,11 +717,52 @@ export default async function (pi: ExtensionAPI) {
|
|
|
577
717
|
},
|
|
578
718
|
});
|
|
579
719
|
|
|
720
|
+
/* ── report_progress (Phase 2, RFC rfc-delegation-identity §3.2): worker → master checkpoint ── */
|
|
721
|
+
if (isSubagent) {
|
|
722
|
+
pi.registerTool({
|
|
723
|
+
name: 'report_progress',
|
|
724
|
+
label: 'Report Progress',
|
|
725
|
+
description:
|
|
726
|
+
'Send the delegating master a short progress update without finishing the task. Use at meaningful checkpoints (a module done, a test went green, a blocker found) — the master sees it in its context but is not interrupted.',
|
|
727
|
+
promptSnippet: 'report_progress: mid-task checkpoint to the delegating master (does not end the task).',
|
|
728
|
+
promptGuidelines: [
|
|
729
|
+
'Call at meaningful checkpoints only — a milestone reached, a risk discovered, a decision the master should know early — not after every tool call.',
|
|
730
|
+
'One or two sentences per call; the final answer still goes in your normal closing message when the task finishes.',
|
|
731
|
+
],
|
|
732
|
+
parameters: Type.Object({
|
|
733
|
+
message: Type.String({ description: 'Short checkpoint update for the delegating master.' }),
|
|
734
|
+
}),
|
|
735
|
+
async execute(_tc, params) {
|
|
736
|
+
const parentPipe = delegation?.parentPipe;
|
|
737
|
+
const message = typeof (params as { message?: unknown })?.message === 'string'
|
|
738
|
+
? (params as { message: string }).message.trim()
|
|
739
|
+
: '';
|
|
740
|
+
if (!message) {
|
|
741
|
+
return { content: [{ type: 'text', text: 'Error: `message` is required (one or two sentences).' }], details: {} };
|
|
742
|
+
}
|
|
743
|
+
if (!parentPipe) {
|
|
744
|
+
return { content: [{ type: 'text', text: 'Error: no delegating master pipe is known to this session; progress cannot be delivered.' }], details: {} };
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const res = await pipeRequest(parentPipe, {
|
|
748
|
+
type: 'progress',
|
|
749
|
+
id: `prog-${Date.now()}`,
|
|
750
|
+
paneId: env?.paneId ?? '',
|
|
751
|
+
text: message,
|
|
752
|
+
}, 5000);
|
|
753
|
+
return res.type === 'ok'
|
|
754
|
+
? { content: [{ type: 'text', text: 'Progress delivered to the master (visible in its context, no interruption).' }], details: {} }
|
|
755
|
+
: { content: [{ type: 'text', text: `Error: master rejected progress report: ${res.message}` }], details: {} };
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return { content: [{ type: 'text', text: `Error: could not reach the master pipe: ${(err as Error)?.message ?? err}` }], details: {} };
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
580
763
|
pi.on('session_shutdown', async () => {
|
|
581
764
|
coordinator.compactionInFlight = false;
|
|
582
765
|
coordinator.intentionalAbort = false;
|
|
583
|
-
sessionFocusPoller.current?.stop();
|
|
584
|
-
sessionFocusPoller.current = null;
|
|
585
766
|
if (pipeServerBox.current) {
|
|
586
767
|
try { pipeServerBox.current.close(); } catch { /* Already closed. */ }
|
|
587
768
|
pipeServerBox.current = null;
|
|
@@ -592,6 +773,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
592
773
|
} catch {
|
|
593
774
|
}
|
|
594
775
|
}
|
|
776
|
+
if (treeSnapshot) {
|
|
777
|
+
try {
|
|
778
|
+
unlinkSync(treeSnapshot.path);
|
|
779
|
+
} catch {
|
|
780
|
+
/* Already swept. */
|
|
781
|
+
}
|
|
782
|
+
treeSnapshot = null;
|
|
783
|
+
}
|
|
595
784
|
client.close();
|
|
596
785
|
});
|
|
597
786
|
|
|
@@ -612,6 +801,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
612
801
|
noticePending: notices.noticePending,
|
|
613
802
|
getSessionId: () => sessionId,
|
|
614
803
|
getBlockedDepth: () => gates.depth(),
|
|
804
|
+
getWorkers: widgetWorkers,
|
|
615
805
|
reconcileOnSettlement,
|
|
616
806
|
withReconcileNotes,
|
|
617
807
|
claimSettleNotice,
|
|
@@ -620,6 +810,66 @@ export default async function (pi: ExtensionAPI) {
|
|
|
620
810
|
jev: { ask: (request, meta) => jevRuntime.ask(request, meta), getMinConfidence: () => effConfig.jev.minConfidence },
|
|
621
811
|
appendRoutingLog,
|
|
622
812
|
});
|
|
813
|
+
|
|
814
|
+
/* ── /pier-tree (D113): full todo × subagent delegation tree; the resident widget is minimal ── */
|
|
815
|
+
pi.registerCommand('pier-tree', {
|
|
816
|
+
description: 'Show the full delegation tree: your todos plus every subagent\'s todos and settled nodes',
|
|
817
|
+
handler: async (args: unknown, ctx: unknown) => {
|
|
818
|
+
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
819
|
+
const lines = renderTreeLines();
|
|
820
|
+
const overlay = typeof args === 'string' && args.trim().toLowerCase() === 'overlay';
|
|
821
|
+
|
|
822
|
+
if (client.available && env) {
|
|
823
|
+
const treeFile = publishTreeSnapshot();
|
|
824
|
+
if (overlay) {
|
|
825
|
+
try {
|
|
826
|
+
const res = await client.openPluginPane({
|
|
827
|
+
pluginId: 'pier.workbench',
|
|
828
|
+
entrypoint: 'tree',
|
|
829
|
+
placement: 'overlay',
|
|
830
|
+
focus: true,
|
|
831
|
+
env: { PIER_PANEL: 'tree', PIER_TREE_FILE: treeFile },
|
|
832
|
+
});
|
|
833
|
+
if (res.mode === 'pane') return;
|
|
834
|
+
} catch {
|
|
835
|
+
// Plugin missing / older herdr: fall back to the in-pane notify view below.
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
const res = await openPierPanel({ client, env }, { panel: 'tree', treeFile });
|
|
839
|
+
if (res === 'popup') return;
|
|
840
|
+
if (res === 'tab') {
|
|
841
|
+
ui?.notify?.('Herdr < 0.9.1: Pier Tree opened in new tab', 'info');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (res === 'busy') {
|
|
845
|
+
ui?.notify?.('Another popup is already open — close it and run /pier-tree again.', 'warning');
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// 'unavailable' / failed overlay → the old in-pane view.
|
|
850
|
+
}
|
|
851
|
+
treeSnapshot = null;
|
|
852
|
+
ui?.notify?.(lines.join('\n'), 'info');
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
/** D113 resync: a resumed master rebuilds worker summaries from herdr pi-todo tokens until the
|
|
857
|
+
* next live push replaces them with full lists. */
|
|
858
|
+
const resyncWorkerTodos = async (): Promise<void> => {
|
|
859
|
+
try {
|
|
860
|
+
const agents = await client.listAgents();
|
|
861
|
+
for (const a of agents) {
|
|
862
|
+
const summary = a.tokens?.['pi-todo'];
|
|
863
|
+
if (typeof summary === 'string' && summary && !workerTodos.has(a.paneId)) {
|
|
864
|
+
workerTodos.set(a.paneId, { summary, ts: Date.now() });
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
todoUi.rerenderWidget?.();
|
|
868
|
+
} catch {
|
|
869
|
+
/* herdr absent or unreachable: live pushes remain the source */
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
pi.on('session_start', () => { void resyncWorkerTodos(); });
|
|
623
873
|
} else {
|
|
624
874
|
const { mountTodoOnly } = await import('./index-worker.ts');
|
|
625
875
|
await mountTodoOnly({
|
package/src/jev-core.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* question needs; instructions are English (CJK is weaker, hence the stricter P0-2 gate); and any
|
|
9
9
|
* answer below its confidence gate counts as unanswered so callers fail open.
|
|
10
10
|
*/
|
|
11
|
+
import { isPlainObject } from './guards.ts';
|
|
11
12
|
import { containsLikelySecret, FAILURE_SIGNAL } from './reducer-core.ts';
|
|
12
13
|
|
|
13
14
|
/* ── Request/response shapes (closed set) ── */
|
|
@@ -36,9 +37,6 @@ export type JevParsed =
|
|
|
36
37
|
| { readonly ok: true; readonly answers: Record<string, JevAnswer>; readonly model: string; readonly usage: JevUsage }
|
|
37
38
|
| { readonly ok: false; readonly reason: string };
|
|
38
39
|
|
|
39
|
-
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
40
|
-
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
41
|
-
}
|
|
42
40
|
|
|
43
41
|
function finiteNumber(v: unknown): number | undefined {
|
|
44
42
|
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Shared Pier panel opening: `/dashboard` and `/pier-tree` both render through one popup entrypoint.
|
|
2
|
+
*
|
|
3
|
+
* Herdr popups are a singleton session resource (`popup.close` has no id) and are session-modal, so
|
|
4
|
+
* the human cannot invoke another slash command while one is open. The command therefore chooses
|
|
5
|
+
* the initial tab via env; tabs are switched inside the panel (`1`/`2`/`Tab`). A `ui_busy` result is
|
|
6
|
+
* surfaced instead of silently retrying as a tab. */
|
|
7
|
+
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
8
|
+
|
|
9
|
+
export type PierPanel = 'dashboard' | 'tree';
|
|
10
|
+
export type PierPanelOpenResult = 'popup' | 'busy' | 'tab' | 'unavailable';
|
|
11
|
+
|
|
12
|
+
export interface PierPanelOpenDeps {
|
|
13
|
+
client: HerdrClientLike;
|
|
14
|
+
env: HerdrEnv | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Open the shared popup on the requested tab. `busy` means another popup/modal owns the session;
|
|
19
|
+
* the caller should tell the human to close it.
|
|
20
|
+
*/
|
|
21
|
+
export async function openPierPanel(
|
|
22
|
+
deps: PierPanelOpenDeps,
|
|
23
|
+
opts: { panel: PierPanel; treeFile?: string | null },
|
|
24
|
+
): Promise<PierPanelOpenResult> {
|
|
25
|
+
const { client, env } = deps;
|
|
26
|
+
if (!client.available || !env) return 'unavailable';
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const res = await client.openPluginPane({
|
|
30
|
+
pluginId: 'pier.workbench',
|
|
31
|
+
entrypoint: 'panel',
|
|
32
|
+
placement: 'popup',
|
|
33
|
+
width: '90%',
|
|
34
|
+
height: '80%',
|
|
35
|
+
focus: true,
|
|
36
|
+
env: {
|
|
37
|
+
PIER_PANEL: opts.panel,
|
|
38
|
+
...(opts.treeFile ? { PIER_TREE_FILE: opts.treeFile } : {}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (res.mode === 'fallback_tab') return 'tab';
|
|
42
|
+
if (res.mode === 'busy') return 'busy';
|
|
43
|
+
return 'popup';
|
|
44
|
+
} catch {
|
|
45
|
+
return 'unavailable';
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/pier-options.ts
CHANGED
|
@@ -47,11 +47,10 @@ export const PIER_OPTIONS: readonly OptionSpec[] = [
|
|
|
47
47
|
{ name: 'PIER_SETTLEMENT_WINDOW_MS', legacy: 'PI_HERDR_SETTLEMENT_WINDOW_MS', fallback: '60000', min: 0, policy: 'settlementWindowMs', description: 'settlement notice window / machine-inject grace / takeover idle' },
|
|
48
48
|
{ name: 'PIER_FOREGROUND_PATIENCE_MS', legacy: 'PI_HERDR_FOREGROUND_PATIENCE_MS', fallback: '300000', min: 0, policy: 'foregroundPatienceMs', description: 'foreground patience before demoting a subagent to background' },
|
|
49
49
|
{ name: 'PIER_SESSION_TTL_SECONDS', legacy: 'PI_HERDR_SESSION_TTL_SECONDS', fallback: '600', min: 0, policy: 'sessionTtlSeconds', description: 'session retention after a subagent exits, before GC' },
|
|
50
|
-
{ name: 'PIER_FOCUS_POLL_MS', fallback: '1500', min: 0, description: 'pane-focus sampling cadence for the heat layout (0 disables; event-first on herdr 0.9.1+)' },
|
|
51
50
|
{ name: 'PIER_TERMINAL_PROMPT', legacy: 'PI_HERDR_TERMINAL_PROMPT', fallback: '(auto from $SHELL)', description: 'prompt strategy for terminal readiness: bash|zsh|powershell|pwsh' },
|
|
52
51
|
{ name: 'PIER_SLIM_FRAME', legacy: 'PI_HERDR_SLIM_FRAME', fallback: '1', description: 'mutation frame around todo-tool cards (0 disables)' },
|
|
53
52
|
{ name: 'PIER_TRACE', legacy: 'PI_HERDR_TRACE', fallback: '', description: 'write diagnostics (renderers, swallowed errors) to stderr (or to this file when it is a path)' },
|
|
54
|
-
{ name: 'PIER_TERM_IDLE_MS', legacy: 'PI_HERDR_TERM_IDLE_MS', fallback: '
|
|
53
|
+
{ name: 'PIER_TERM_IDLE_MS', legacy: 'PI_HERDR_TERM_IDLE_MS', fallback: '300000', min: 1, description: 'idle time before the second cleanup nudge for an open terminal (per-terminal max 2; 0/NaN falls back to the default)' },
|
|
55
54
|
{ name: 'PIER_TERM_GRACE_MS', legacy: 'PI_HERDR_TERM_GRACE_MS', fallback: '30000', min: 1, description: 'settle grace before the terminal idle nudge (0/NaN falls back to the default)' },
|
|
56
55
|
{ name: 'PIER_TERM_READ_MAX', legacy: 'PI_HERDR_TERM_READ_MAX', fallback: '8000', min: 1, description: 'characters returned per terminal read (0/NaN falls back to the default)' },
|
|
57
56
|
{ name: 'PIER_TODO_GRACE_MS', legacy: 'PI_HERDR_TODO_GRACE_MS', fallback: '30000', min: 1, description: 'settle grace before the unfinished-todo reminder (0/NaN falls back to the default)' },
|
package/src/pipe-channel.ts
CHANGED
|
@@ -58,6 +58,23 @@ export type PipeRequest =
|
|
|
58
58
|
id: string;
|
|
59
59
|
role: string;
|
|
60
60
|
}
|
|
61
|
+
| {
|
|
62
|
+
/** Phase 2 (RFC rfc-delegation-identity §3.2): worker → master progress report. The master
|
|
63
|
+
* injects it as a display-less custom message (no turn, no visible chat noise). */
|
|
64
|
+
type: 'progress';
|
|
65
|
+
id: string;
|
|
66
|
+
paneId: string;
|
|
67
|
+
text: string;
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
/** D113 (RFC rfc-unified-widget): worker → master full todo push on every todo_write, so the
|
|
71
|
+
* master's resident widget and /pier-tree render live worker state without polling herdr. */
|
|
72
|
+
type: 'todo_sync';
|
|
73
|
+
id: string;
|
|
74
|
+
paneId: string;
|
|
75
|
+
items: Array<{ content: string; status: string; blocker?: string }>;
|
|
76
|
+
summary: string;
|
|
77
|
+
}
|
|
61
78
|
| {
|
|
62
79
|
/** D50: fast-path settlement reply from child to requester, with summary and session path for progressive disclosure. */
|
|
63
80
|
type: 'reply';
|