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/core/todo.ts
CHANGED
|
@@ -15,6 +15,10 @@ import { planTodoReadHook } from '../todo-read-hook.ts';
|
|
|
15
15
|
import { TODO_REMINDER_CUSTOM_TYPE, planStopTodoReminder, todoReminderGraceMs } from '../todo-reminder-core.ts';
|
|
16
16
|
import { makeProgressUpdate } from '../subagent-core.ts';
|
|
17
17
|
import { TODO_DETAILS_KEY, TODO_TOOL_NAME, formatTodoConfirmation, type TodoItem } from '../vocab.ts';
|
|
18
|
+
import { toolError } from '../tool-error.ts'
|
|
19
|
+
|
|
20
|
+
/** Raw tool arguments: every field is validated inside the action handlers. */
|
|
21
|
+
type ToolParams = Record<string, unknown> | undefined;;
|
|
18
22
|
import { formatAge, isArchived } from '../stale-core.ts';
|
|
19
23
|
import {
|
|
20
24
|
TODO_EDIT_CUSTOM_TYPE,
|
|
@@ -27,10 +31,15 @@ import {
|
|
|
27
31
|
validateTodos,
|
|
28
32
|
} from '../todo-core.ts';
|
|
29
33
|
import { anchorTodoRange, formatTodoSummary, renderTodoGroups } from '../todo-window.ts';
|
|
34
|
+
import { swallow } from '../swallow.ts';
|
|
30
35
|
|
|
31
36
|
export interface TodoUiSlot {
|
|
32
37
|
/** Lets index lifecycle events render through the currently mounted plugin. */
|
|
33
38
|
renderWidget: (ctx: unknown) => void;
|
|
39
|
+
/** Re-render from the last seen context without a fresh event (human-gate open/close). */
|
|
40
|
+
rerenderWidget?: () => void;
|
|
41
|
+
/** Cancel any pending unfinished-todo reminder timer. */
|
|
42
|
+
cancelReminder?: () => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
interface TodoDeps {
|
|
@@ -41,10 +50,14 @@ interface TodoDeps {
|
|
|
41
50
|
mirrorTodos: () => void;
|
|
42
51
|
appendEntry: (customType: string, data: unknown) => void;
|
|
43
52
|
state: TodoUiSlot;
|
|
53
|
+
/** Human-gate depth (ask_user_question / herdr:blocked); >0 collapses the widget to one line. */
|
|
54
|
+
getBlockedDepth?: () => number;
|
|
44
55
|
/** Master-only unfinished-todo reminder; omitted on worker panes. */
|
|
45
56
|
stopReminder?: {
|
|
46
57
|
getBlockedDepth: () => number;
|
|
47
58
|
getRunningSubs: () => number;
|
|
59
|
+
isCompactionInFlight?: () => boolean;
|
|
60
|
+
isIntentionalAbort?: () => boolean;
|
|
48
61
|
};
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -67,8 +80,18 @@ export const WIDGET_MAX_LINES = 10;
|
|
|
67
80
|
* completions. A +N line points to /todos when the full plan cannot fit. This fixes 01a03c0d,
|
|
68
81
|
* where active work scrolled out while only the final entries remained visible.
|
|
69
82
|
*/
|
|
70
|
-
export function widgetLines(
|
|
83
|
+
export function widgetLines(
|
|
84
|
+
items: readonly TodoItem[],
|
|
85
|
+
opts?: { archivedAgeMs?: number | null; blockedDepth?: number | null },
|
|
86
|
+
): string[] {
|
|
71
87
|
if (items.length === 0) return [];
|
|
88
|
+
// Human gate open (ask_user_question waiting): the question + editor own the fixed area, so the
|
|
89
|
+
// widget collapses to the one-line summary — /todos still reaches the full list. Without this,
|
|
90
|
+
// a 10-line widget plus a multi-line question leaves almost no scrollable transcript
|
|
91
|
+
// (user-reported: both blocks visible ⇒ history range too small).
|
|
92
|
+
if ((opts?.blockedDepth ?? 0) > 0) {
|
|
93
|
+
return [`${formatTodoSummary(items)} · /todos 全量`];
|
|
94
|
+
}
|
|
72
95
|
// Collapse archived plans to two lines so stale work cannot monopolize the widget.
|
|
73
96
|
if (opts?.archivedAgeMs != null) {
|
|
74
97
|
return [
|
|
@@ -95,7 +118,7 @@ export function widgetLines(items: readonly TodoItem[], opts?: { archivedAgeMs?:
|
|
|
95
118
|
|
|
96
119
|
export default function todoPlugin(ctx: Context): void {
|
|
97
120
|
const surface = ctx.get('pi-herdr.surface') as PiSurface<object>;
|
|
98
|
-
const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state, stopReminder } =
|
|
121
|
+
const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state, stopReminder, getBlockedDepth } =
|
|
99
122
|
ctx.get('pi-herdr.todo-deps') as TodoDeps;
|
|
100
123
|
const scoped = surface.forModule(import.meta.url);
|
|
101
124
|
|
|
@@ -115,9 +138,15 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
115
138
|
: undefined;
|
|
116
139
|
}
|
|
117
140
|
|
|
141
|
+
/** Gate transitions fire outside lifecycle events; remember one ctx so rerenderWidget can target the live ui. */
|
|
142
|
+
let lastEventCtx: unknown = null;
|
|
118
143
|
function renderWidget(eventCtx: unknown): void {
|
|
144
|
+
if (eventCtx !== null && eventCtx !== undefined) lastEventCtx = eventCtx;
|
|
119
145
|
try {
|
|
120
|
-
widgetUi(eventCtx)?.setWidget?.('todos', widgetLines(todos.items, {
|
|
146
|
+
widgetUi(eventCtx)?.setWidget?.('todos', widgetLines(todos.items, {
|
|
147
|
+
archivedAgeMs: archivedAgeMs(),
|
|
148
|
+
blockedDepth: getBlockedDepth?.() ?? 0,
|
|
149
|
+
}));
|
|
121
150
|
} catch {
|
|
122
151
|
/* Older pi versions may omit widget support without disabling todo tracking. */
|
|
123
152
|
}
|
|
@@ -125,6 +154,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
125
154
|
|
|
126
155
|
// Let index lifecycle hooks call the current plugin implementation.
|
|
127
156
|
state.renderWidget = renderWidget;
|
|
157
|
+
state.rerenderWidget = () => renderWidget(lastEventCtx);
|
|
128
158
|
|
|
129
159
|
/* ── Read hook: D39 reminders and stale-plan thawing ────────────── */
|
|
130
160
|
let todoReadTurn = 0;
|
|
@@ -133,7 +163,6 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
133
163
|
let lastWriteTurn: number | null = null;
|
|
134
164
|
let staleNotices = 0;
|
|
135
165
|
let lastStaleGuardTurn: number | null = null;
|
|
136
|
-
let lastArchivedMirror = false;
|
|
137
166
|
|
|
138
167
|
// Any real edit ends the current stale period because the plan is active again.
|
|
139
168
|
todos.on('todo.updated', () => {
|
|
@@ -168,10 +197,12 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
168
197
|
const edits = todos.items.map((it) => ({ op: 'rm' as const, content: it.content }));
|
|
169
198
|
try {
|
|
170
199
|
appendEntry(TODO_EDIT_CUSTOM_TYPE, { version: 1, edits, ts: Date.now() });
|
|
171
|
-
} catch {
|
|
172
|
-
|
|
200
|
+
} catch (err) {
|
|
201
|
+
// Persistence is best-effort because the in-memory clear must still proceed, but a silent
|
|
202
|
+
// failure here is exactly how the archive/replay path broke unnoticed before (SA-13).
|
|
203
|
+
swallow('todo.persist-archive', err);
|
|
173
204
|
}
|
|
174
|
-
todos.replace([]);
|
|
205
|
+
todos.replace([], { source: 'archive' });
|
|
175
206
|
}
|
|
176
207
|
todoReadTurn += 1;
|
|
177
208
|
if (!plan.inject) return;
|
|
@@ -200,6 +231,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
200
231
|
todoReminderTimer = null;
|
|
201
232
|
}
|
|
202
233
|
}
|
|
234
|
+
state.cancelReminder = cancelTodoReminder;
|
|
203
235
|
scoped.on('turn_end', async (event: unknown) => {
|
|
204
236
|
if (event === null || typeof event !== 'object' || !('message' in event)) return;
|
|
205
237
|
const msg = (event as { message: unknown }).message;
|
|
@@ -213,8 +245,12 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
213
245
|
scoped.on('session_shutdown', () => cancelTodoReminder());
|
|
214
246
|
scoped.on('agent_settled', async () => {
|
|
215
247
|
cancelTodoReminder();
|
|
248
|
+
const inFlight = stopReminder.isCompactionInFlight?.() ?? false;
|
|
249
|
+
const intentional = stopReminder.isIntentionalAbort?.() ?? false;
|
|
216
250
|
const plan = planStopTodoReminder({
|
|
217
251
|
lastStopReason: lastAssistantStopReason,
|
|
252
|
+
intentionalAbort: intentional,
|
|
253
|
+
compactionInFlight: inFlight,
|
|
218
254
|
reminders: todoReminders,
|
|
219
255
|
runningSubs: stopReminder.getRunningSubs(),
|
|
220
256
|
blockedDepth: stopReminder.getBlockedDepth(),
|
|
@@ -224,6 +260,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
224
260
|
const content = plan.content;
|
|
225
261
|
todoReminderTimer = setTimeout(() => {
|
|
226
262
|
todoReminderTimer = null;
|
|
263
|
+
if (stopReminder.isCompactionInFlight?.()) return;
|
|
227
264
|
void (async () => {
|
|
228
265
|
const send = pi.sendMessage;
|
|
229
266
|
if (typeof send !== 'function') return;
|
|
@@ -270,15 +307,13 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
270
307
|
{ description: 'The COMPLETE task list; this call replaces the previous list' },
|
|
271
308
|
),
|
|
272
309
|
}),
|
|
273
|
-
async execute(toolCallId, params, signal, onUpdate, eventCtx) {
|
|
310
|
+
async execute(toolCallId: string, params: ToolParams, signal: AbortSignal | undefined, onUpdate: ((update: unknown) => void) | undefined, eventCtx: unknown) {
|
|
274
311
|
void toolCallId;
|
|
275
312
|
void signal;
|
|
276
313
|
const result = validateTodos(params?.todos, allowParallelInProgress);
|
|
277
314
|
if (!result.ok) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
details: {},
|
|
281
|
-
};
|
|
315
|
+
// A1: an invalid list is a hard failure — throw so pi flags isError for the model.
|
|
316
|
+
return toolError(result.error ?? 'invalid todo list');
|
|
282
317
|
}
|
|
283
318
|
const strictMode = todos.config.strict; // D75 phase 2 keeps policy in the service config.
|
|
284
319
|
let next = result.items!;
|
|
@@ -301,7 +336,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
301
336
|
// D36/D37 surface completion transitions and regressions to the caller.
|
|
302
337
|
const completed = completionTransitions(todos.items, next);
|
|
303
338
|
const reverted = revertedCompleted(todos.items, next);
|
|
304
|
-
todos.replace(next);
|
|
339
|
+
todos.replace(next, { source: 'tool' });
|
|
305
340
|
if (todos.items.length > maxItems) {
|
|
306
341
|
onUpdate?.(makeProgressUpdate(`Note: list has ${todos.items.length} entries; consider keeping it under ${maxItems}.`));
|
|
307
342
|
}
|
|
@@ -351,7 +386,7 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
351
386
|
const content = candidates[0];
|
|
352
387
|
const edit = { op: opArg as 'done' | 'drop' | 'rm' | 'unblock', content };
|
|
353
388
|
const before = todos.items;
|
|
354
|
-
todos.applyEdits([edit]);
|
|
389
|
+
todos.applyEdits([edit], { source: 'human' });
|
|
355
390
|
if (listsEqual(before, todos.items)) {
|
|
356
391
|
ui?.notify?.(`no change: "${content}" is already in that state`, 'info');
|
|
357
392
|
return;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { HerdrClientLike, HerdrEnv } from './herdr-client.ts';
|
|
2
|
+
import type { TodoItem } from './vocab.ts';
|
|
3
|
+
|
|
4
|
+
export interface DashboardCommandDeps {
|
|
5
|
+
pi: {
|
|
6
|
+
registerCommand(name: string, options: { description: string; handler: (args: unknown, ctx: unknown) => Promise<void> }): void;
|
|
7
|
+
};
|
|
8
|
+
client: HerdrClientLike;
|
|
9
|
+
env: HerdrEnv | null;
|
|
10
|
+
getTodoItems?: () => readonly TodoItem[];
|
|
11
|
+
getHeldLocks?: () => readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatStandaloneDashboard(opts: {
|
|
15
|
+
todos: readonly TodoItem[];
|
|
16
|
+
locks: readonly string[];
|
|
17
|
+
now?: number;
|
|
18
|
+
}): string {
|
|
19
|
+
const lines: string[] = [];
|
|
20
|
+
const timeStr = new Date(opts.now ?? Date.now()).toTimeString().split(' ')[0] ?? '';
|
|
21
|
+
lines.push(`==================== PIER OPS DASHBOARD ==================== [${timeStr}] (standalone)`);
|
|
22
|
+
lines.push('Mode: Standalone (Non-Herdr / Local Session)');
|
|
23
|
+
lines.push('--------------------------------------------------------------------------------');
|
|
24
|
+
|
|
25
|
+
const todos = opts.todos;
|
|
26
|
+
if (todos.length > 0) {
|
|
27
|
+
const inProg = todos.filter((t) => t.status === 'in_progress');
|
|
28
|
+
const blocked = todos.filter((t) => t.status === 'blocked');
|
|
29
|
+
const done = todos.filter((t) => t.status === 'completed');
|
|
30
|
+
const pending = todos.filter((t) => t.status === 'pending');
|
|
31
|
+
lines.push(
|
|
32
|
+
`Todos: ${todos.length} total (${done.length} done, ${inProg.length} working, ${blocked.length} blocked, ${pending.length} pending)`,
|
|
33
|
+
);
|
|
34
|
+
if (inProg.length > 0) {
|
|
35
|
+
lines.push(`Active: ▶ ${inProg.map((t) => t.content).join(', ')}`);
|
|
36
|
+
}
|
|
37
|
+
if (blocked.length > 0) {
|
|
38
|
+
lines.push(`Blocked: ■ ${blocked.map((t) => `${t.content}${t.blocker ? ` (${t.blocker})` : ''}`).join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
if (inProg.length === 0 && blocked.length === 0 && done.length > 0) {
|
|
41
|
+
lines.push(`Completed: ✓ ${done.slice(-3).map((t) => t.content).join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
lines.push('Todos: (none)');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const locks = opts.locks;
|
|
48
|
+
lines.push(`Write Locks: ${locks.length > 0 ? locks.join(', ') : '(none)'}`);
|
|
49
|
+
lines.push('--------------------------------------------------------------------------------');
|
|
50
|
+
lines.push('Tip: Inside Herdr 0.9.1, /dashboard opens the interactive modal popup.');
|
|
51
|
+
lines.push('================================================================================');
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function installDashboardCommand(deps: DashboardCommandDeps): void {
|
|
56
|
+
deps.pi.registerCommand('dashboard', {
|
|
57
|
+
description: 'Open the Pier Ops Dashboard (modal popup in Herdr 0.9.1, tab fallback, or local TUI view)',
|
|
58
|
+
handler: async (_args, ctx) => {
|
|
59
|
+
const ui = (ctx as { ui?: { notify?: (text: string, level?: string) => void } }).ui;
|
|
60
|
+
|
|
61
|
+
// Level 1 / Level 2: Herdr environment
|
|
62
|
+
if (deps.client.available && deps.env) {
|
|
63
|
+
try {
|
|
64
|
+
const res = await deps.client.openPluginPane({
|
|
65
|
+
pluginId: 'pier.workbench',
|
|
66
|
+
entrypoint: 'dashboard',
|
|
67
|
+
placement: 'popup',
|
|
68
|
+
width: '80%',
|
|
69
|
+
height: '80%',
|
|
70
|
+
focus: true,
|
|
71
|
+
});
|
|
72
|
+
if (res.mode === 'fallback_tab') {
|
|
73
|
+
ui?.notify?.('Herdr < 0.9.1: Pier Dashboard opened in new tab', 'info');
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
} catch {
|
|
77
|
+
// If plugin is not installed or socket error occurs, fall through to Level 3 local view
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Level 3: Standalone / Non-Herdr fallback
|
|
82
|
+
const text = formatStandaloneDashboard({
|
|
83
|
+
todos: deps.getTodoItems?.() ?? [],
|
|
84
|
+
locks: deps.getHeldLocks?.() ?? [],
|
|
85
|
+
});
|
|
86
|
+
ui?.notify?.(text, 'info');
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|