pi-pier 0.2.4 → 0.2.6
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 +37 -8
- package/package.json +1 -1
- package/skills/pi-herdr-workflow/SKILL.md +96 -15
- package/src/core/subagent.ts +246 -332
- package/src/core/terminal.ts +200 -232
- package/src/core/todo.ts +78 -79
- package/src/index-master.ts +4 -2
- package/src/index-runtime.ts +6 -2
- package/src/index-worker.ts +11 -5
- package/src/index.ts +72 -31
- package/src/notice-buffer.ts +2 -2
- package/src/roles/master.json +4 -4
- package/src/roles/worker-default.json +3 -12
- package/src/slim-frame.ts +120 -11
- package/src/subagent-core.ts +3 -3
- package/src/subagent-poll-loop.ts +1 -1
- package/src/terminal-core.ts +3 -3
- package/src/todo-window.ts +82 -0
package/src/core/todo.ts
CHANGED
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A Cordis plugin keeps the master surface hot-swappable while allowing workers to mount the
|
|
5
5
|
* one-shot subset directly. Injected services preserve the D79 registration boundary and keep
|
|
6
|
-
* session-owned todo state in index. This module owns the tool, command, widget,
|
|
7
|
-
* reconciliation, mirroring, and agent reporting stay with the
|
|
8
|
-
* dependency cycle.
|
|
6
|
+
* session-owned todo state in index. This module owns the tool, command, widget, read hook,
|
|
7
|
+
* and master stop reminder; reconciliation, mirroring, and agent reporting stay with the
|
|
8
|
+
* session state layer to avoid a dependency cycle.
|
|
9
9
|
*/
|
|
10
10
|
import { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import { Type } from 'typebox';
|
|
12
12
|
import type { PiSurface } from '../pi-surface.ts';
|
|
13
13
|
import type { TodosService } from '../todos-service.ts';
|
|
14
14
|
import { planTodoReadHook } from '../todo-read-hook.ts';
|
|
15
|
+
import { TODO_REMINDER_CUSTOM_TYPE, planStopTodoReminder, todoReminderGraceMs } from '../todo-reminder-core.ts';
|
|
15
16
|
import { makeProgressUpdate } from '../subagent-core.ts';
|
|
16
|
-
import { TODO_DETAILS_KEY, TODO_TOOL_NAME, formatTodoConfirmation, type TodoItem
|
|
17
|
+
import { TODO_DETAILS_KEY, TODO_TOOL_NAME, formatTodoConfirmation, type TodoItem } from '../vocab.ts';
|
|
17
18
|
import { formatAge, isArchived } from '../stale-core.ts';
|
|
18
19
|
import {
|
|
19
20
|
TODO_EDIT_CUSTOM_TYPE,
|
|
20
21
|
completionTransitions,
|
|
21
|
-
countTodos,
|
|
22
22
|
fuzzyFind,
|
|
23
23
|
listsEqual,
|
|
24
24
|
makeSnapshot,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
revertedCompleted,
|
|
27
27
|
validateTodos,
|
|
28
28
|
} from '../todo-core.ts';
|
|
29
|
+
import { anchorTodoRange, formatTodoSummary, renderTodoGroups } from '../todo-window.ts';
|
|
29
30
|
|
|
30
31
|
export interface TodoUiSlot {
|
|
31
32
|
/** Lets index lifecycle events render through the currently mounted plugin. */
|
|
@@ -40,6 +41,11 @@ interface TodoDeps {
|
|
|
40
41
|
mirrorTodos: () => void;
|
|
41
42
|
appendEntry: (customType: string, data: unknown) => void;
|
|
42
43
|
state: TodoUiSlot;
|
|
44
|
+
/** Master-only unfinished-todo reminder; omitted on worker panes. */
|
|
45
|
+
stopReminder?: {
|
|
46
|
+
getBlockedDepth: () => number;
|
|
47
|
+
getRunningSubs: () => number;
|
|
48
|
+
};
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
const TOOL_DESCRIPTION = [
|
|
@@ -52,37 +58,9 @@ const TOOL_DESCRIPTION = [
|
|
|
52
58
|
'Delegated work belongs on this list too. When you hand an entry to a subagent, append ` <sub>` to its content (the subagent is doing it, not you); when the subagent settles, a matching entry is auto-completed — you will see "Reconciled:" in the settlement note. If no auto-match fired, update the entry yourself.',
|
|
53
59
|
].join(' ');
|
|
54
60
|
|
|
55
|
-
const TODO_MARKS: Record<TodoStatus, string> = {
|
|
56
|
-
pending: '○',
|
|
57
|
-
in_progress: '▶',
|
|
58
|
-
completed: '✓',
|
|
59
|
-
blocked: '■',
|
|
60
|
-
abandoned: '✗',
|
|
61
|
-
};
|
|
62
|
-
|
|
63
61
|
/** Stay within pi interactive mode's ten-line widget cap instead of relying on head truncation. */
|
|
64
62
|
export const WIDGET_MAX_LINES = 10;
|
|
65
63
|
|
|
66
|
-
/** Share phase rendering between the widget and /todos so both views order entries identically. */
|
|
67
|
-
function renderGroups(items: readonly TodoItem[]): string[] {
|
|
68
|
-
const groups = new Map<string, TodoItem[]>();
|
|
69
|
-
for (const it of items) {
|
|
70
|
-
const key = it.phase ?? '';
|
|
71
|
-
const arr = groups.get(key) ?? [];
|
|
72
|
-
arr.push(it);
|
|
73
|
-
groups.set(key, arr);
|
|
74
|
-
}
|
|
75
|
-
const lines: string[] = [];
|
|
76
|
-
for (const [phase, list] of groups) {
|
|
77
|
-
if (phase) lines.push(` [${phase}]`);
|
|
78
|
-
for (const it of list) {
|
|
79
|
-
const suffix = it.status === 'blocked' && it.blocker ? ` — ${it.blocker}` : '';
|
|
80
|
-
lines.push(` ${TODO_MARKS[it.status]} ${it.content}${suffix}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return lines;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
64
|
/**
|
|
87
65
|
* Keep the active task visible within WIDGET_MAX_LINES instead of truncating to a fixed head or
|
|
88
66
|
* tail. The window anchors on the first in-progress item, then the last open item, then recent
|
|
@@ -91,21 +69,23 @@ function renderGroups(items: readonly TodoItem[]): string[] {
|
|
|
91
69
|
*/
|
|
92
70
|
export function widgetLines(items: readonly TodoItem[], opts?: { archivedAgeMs?: number | null }): string[] {
|
|
93
71
|
if (items.length === 0) return [];
|
|
94
|
-
const c = countTodos(items);
|
|
95
72
|
// Collapse archived plans to two lines so stale work cannot monopolize the widget.
|
|
96
73
|
if (opts?.archivedAgeMs != null) {
|
|
97
74
|
return [
|
|
98
|
-
|
|
75
|
+
`${formatTodoSummary(items)} · archived ${formatAge(opts.archivedAgeMs)}`,
|
|
99
76
|
' archived — /todos 全量',
|
|
100
77
|
];
|
|
101
78
|
}
|
|
102
79
|
|
|
103
80
|
// Reserve summary and overflow lines, then shrink around the anchor because phase headers also consume the budget.
|
|
104
81
|
const renderBudget = WIDGET_MAX_LINES - 1 - 1;
|
|
105
|
-
const [start, end] =
|
|
82
|
+
const [start, end] = anchorTodoRange(
|
|
83
|
+
items,
|
|
84
|
+
(s, e) => renderTodoGroups(items.slice(s, e)).length <= renderBudget,
|
|
85
|
+
);
|
|
106
86
|
const kept = items.slice(start, end);
|
|
107
87
|
const hidden = items.filter((_, i) => i < start || i >= end);
|
|
108
|
-
const lines = [
|
|
88
|
+
const lines = [formatTodoSummary(items), ...renderTodoGroups(kept)];
|
|
109
89
|
if (hidden.length > 0) {
|
|
110
90
|
const hiddenCompleted = hidden.filter((it) => it.status === 'completed').length;
|
|
111
91
|
lines.push(` +${hidden.length} hidden (${hiddenCompleted}✓) · /todos 全量`);
|
|
@@ -113,47 +93,9 @@ export function widgetLines(items: readonly TodoItem[], opts?: { archivedAgeMs?:
|
|
|
113
93
|
return lines;
|
|
114
94
|
}
|
|
115
95
|
|
|
116
|
-
/**
|
|
117
|
-
* Expand around the active anchor within the rendered-line budget, favoring upcoming work while
|
|
118
|
-
* guaranteeing that the anchor remains visible.
|
|
119
|
-
*/
|
|
120
|
-
function anchorRange(items: readonly TodoItem[], renderBudget: number): [number, number] {
|
|
121
|
-
if (renderGroups(items).length <= renderBudget) return [0, items.length];
|
|
122
|
-
const anchor = items.findIndex((it) => it.status === 'in_progress');
|
|
123
|
-
const anchorIdx = anchor >= 0
|
|
124
|
-
? anchor
|
|
125
|
-
: items.reduce((acc, it, i) => (it.status !== 'completed' ? i : acc), -1);
|
|
126
|
-
if (anchorIdx < 0) {
|
|
127
|
-
// Completed plans keep the most recent tail visible within the line budget.
|
|
128
|
-
let end = items.length;
|
|
129
|
-
while (renderGroups(items.slice(0, end)).length > renderBudget && end > 1) end -= 1;
|
|
130
|
-
return [0, end];
|
|
131
|
-
}
|
|
132
|
-
let start = anchorIdx;
|
|
133
|
-
let end = anchorIdx + 1;
|
|
134
|
-
const fits = () => renderGroups(items.slice(start, end)).length <= renderBudget;
|
|
135
|
-
let growTail = true;
|
|
136
|
-
while ((end < items.length || start > 0) && (end - start) < items.length) {
|
|
137
|
-
if (growTail && end < items.length) {
|
|
138
|
-
end += 1;
|
|
139
|
-
if (!fits()) { end -= 1; break; }
|
|
140
|
-
} else if (start > 0) {
|
|
141
|
-
start -= 1;
|
|
142
|
-
if (!fits()) { start += 1; break; }
|
|
143
|
-
} else if (end < items.length) {
|
|
144
|
-
end += 1;
|
|
145
|
-
if (!fits()) { end -= 1; break; }
|
|
146
|
-
} else {
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
growTail = !growTail; // Alternate sides so both context and upcoming work remain visible.
|
|
150
|
-
}
|
|
151
|
-
return [start, end];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
96
|
export default function todoPlugin(ctx: Context): void {
|
|
155
97
|
const surface = ctx.get('pi-herdr.surface') as PiSurface<object>;
|
|
156
|
-
const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state } =
|
|
98
|
+
const { todos, allowParallelInProgress, maxItems, mirrorTodos, appendEntry, state, stopReminder } =
|
|
157
99
|
ctx.get('pi-herdr.todo-deps') as TodoDeps;
|
|
158
100
|
const scoped = surface.forModule(import.meta.url);
|
|
159
101
|
|
|
@@ -242,6 +184,64 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
242
184
|
};
|
|
243
185
|
});
|
|
244
186
|
|
|
187
|
+
if (stopReminder) {
|
|
188
|
+
const pi = surface.raw as {
|
|
189
|
+
sendMessage?: (
|
|
190
|
+
message: { customType: string; content: string; display?: boolean; details?: Record<string, unknown> },
|
|
191
|
+
opts?: { deliverAs?: string; triggerTurn?: boolean },
|
|
192
|
+
) => Promise<void>;
|
|
193
|
+
};
|
|
194
|
+
let todoReminders = 0;
|
|
195
|
+
let lastAssistantStopReason: string | null = null;
|
|
196
|
+
let todoReminderTimer: NodeJS.Timeout | null = null;
|
|
197
|
+
function cancelTodoReminder(): void {
|
|
198
|
+
if (todoReminderTimer !== null) {
|
|
199
|
+
clearTimeout(todoReminderTimer);
|
|
200
|
+
todoReminderTimer = null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
scoped.on('turn_end', async (event: unknown) => {
|
|
204
|
+
if (event === null || typeof event !== 'object' || !('message' in event)) return;
|
|
205
|
+
const msg = (event as { message: unknown }).message;
|
|
206
|
+
if (msg === null || typeof msg !== 'object') return;
|
|
207
|
+
const { role, stopReason } = msg as { role?: unknown; stopReason?: unknown };
|
|
208
|
+
if (role === 'assistant' && typeof stopReason === 'string') {
|
|
209
|
+
lastAssistantStopReason = stopReason;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
scoped.on('agent_start', () => cancelTodoReminder());
|
|
213
|
+
scoped.on('session_shutdown', () => cancelTodoReminder());
|
|
214
|
+
scoped.on('agent_settled', async () => {
|
|
215
|
+
cancelTodoReminder();
|
|
216
|
+
const plan = planStopTodoReminder({
|
|
217
|
+
lastStopReason: lastAssistantStopReason,
|
|
218
|
+
reminders: todoReminders,
|
|
219
|
+
runningSubs: stopReminder.getRunningSubs(),
|
|
220
|
+
blockedDepth: stopReminder.getBlockedDepth(),
|
|
221
|
+
items: todos.items,
|
|
222
|
+
});
|
|
223
|
+
if (!plan.due || plan.content == null) return;
|
|
224
|
+
const content = plan.content;
|
|
225
|
+
todoReminderTimer = setTimeout(() => {
|
|
226
|
+
todoReminderTimer = null;
|
|
227
|
+
void (async () => {
|
|
228
|
+
const send = pi.sendMessage;
|
|
229
|
+
if (typeof send !== 'function') return;
|
|
230
|
+
try {
|
|
231
|
+
await send(
|
|
232
|
+
{ customType: TODO_REMINDER_CUSTOM_TYPE, content, display: true },
|
|
233
|
+
{ deliverAs: 'followUp', triggerTurn: true },
|
|
234
|
+
);
|
|
235
|
+
todoReminders += 1;
|
|
236
|
+
} catch {
|
|
237
|
+
/* Delivery failure is non-fatal. */
|
|
238
|
+
}
|
|
239
|
+
})();
|
|
240
|
+
}, todoReminderGraceMs());
|
|
241
|
+
todoReminderTimer.unref?.();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
245
|
/* ── todo_write tool ───────────────────────────────────────────── */
|
|
246
246
|
scoped.registerTool({
|
|
247
247
|
name: TODO_TOOL_NAME,
|
|
@@ -372,14 +372,13 @@ export default function todoPlugin(ctx: Context): void {
|
|
|
372
372
|
return;
|
|
373
373
|
}
|
|
374
374
|
// /todos is the unbounded view because the widget intentionally prioritizes active context.
|
|
375
|
-
const body =
|
|
375
|
+
const body = renderTodoGroups(todos.items);
|
|
376
376
|
if (body.length === 0) {
|
|
377
377
|
ui?.notify?.('todo list is empty', 'info');
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
|
-
const c = countTodos(todos.items);
|
|
381
380
|
const age = archivedAgeMs();
|
|
382
|
-
const head =
|
|
381
|
+
const head = formatTodoSummary(todos.items)
|
|
383
382
|
+ (age != null ? ` · archived ${formatAge(age)}(不再注入)` : '');
|
|
384
383
|
ui?.notify?.([head, ...body].join('\n'), 'info');
|
|
385
384
|
},
|
package/src/index-master.ts
CHANGED
|
@@ -89,6 +89,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
89
89
|
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
90
90
|
},
|
|
91
91
|
state: m.todoUi,
|
|
92
|
+
stopReminder: {
|
|
93
|
+
getBlockedDepth: m.getBlockedDepth,
|
|
94
|
+
getRunningSubs: () => m.port.current?.listRunningSubs().length ?? 0,
|
|
95
|
+
},
|
|
92
96
|
});
|
|
93
97
|
await loadEntry(sessionRoot, useLoader, './core/todo.ts', todoPlugin);
|
|
94
98
|
|
|
@@ -101,12 +105,10 @@ export async function mountMasterPlugins(m: MasterPluginMount): Promise<void> {
|
|
|
101
105
|
deliverNotice: m.deliverNotice,
|
|
102
106
|
noticePending: m.noticePending,
|
|
103
107
|
getSessionId: m.getSessionId,
|
|
104
|
-
getBlockedDepth: m.getBlockedDepth,
|
|
105
108
|
reconcileOnSettlement: m.reconcileOnSettlement,
|
|
106
109
|
withReconcileNotes: m.withReconcileNotes,
|
|
107
110
|
claimSettleNotice: m.claimSettleNotice,
|
|
108
111
|
terminalState: terminalDeps.state,
|
|
109
|
-
todos: m.todos,
|
|
110
112
|
});
|
|
111
113
|
await loadEntry(sessionRoot, useLoader, './core/subagent.ts', subagentPlugin);
|
|
112
114
|
|
package/src/index-runtime.ts
CHANGED
|
@@ -10,14 +10,18 @@ import { detectHerdrEnv } from './herdr-client.ts';
|
|
|
10
10
|
export interface IndexMode {
|
|
11
11
|
/** PI_HERDR_SUBAGENT=1 → worker pane (no subagent tools, no cordis loader). */
|
|
12
12
|
readonly isSubagent: boolean;
|
|
13
|
-
/**
|
|
13
|
+
/** HERDR_ENV + socket + pane are present. */
|
|
14
|
+
readonly hasHerdr: boolean;
|
|
15
|
+
/** herdr pane that is not a worker → apply builtin master manifest and mount workbench plugins. */
|
|
14
16
|
readonly composeMaster: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export function planIndexMode(env: NodeJS.ProcessEnv = process.env): IndexMode {
|
|
18
20
|
const isSubagent = env.PI_HERDR_SUBAGENT === '1';
|
|
21
|
+
const hasHerdr = Boolean(detectHerdrEnv(env));
|
|
19
22
|
return {
|
|
20
23
|
isSubagent,
|
|
21
|
-
|
|
24
|
+
hasHerdr,
|
|
25
|
+
composeMaster: hasHerdr && !isSubagent,
|
|
22
26
|
};
|
|
23
27
|
}
|
package/src/index-worker.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Todo-only plugin mount (bare cordis root, no loader/hmr).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Used by worker panes and by standalone pi outside herdr, so bootstrap/subagent/terminal
|
|
5
|
+
* stay out of those processes.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
8
|
import { Context } from '@deepseek-ai/cordis';
|
|
@@ -11,14 +11,19 @@ import todoPlugin from './core/todo.ts';
|
|
|
11
11
|
import type { TodosService } from './todos-service.ts';
|
|
12
12
|
import type { TodoUiSlot } from './core/todo.ts';
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface TodoOnlyMount {
|
|
15
15
|
pi: ExtensionAPI;
|
|
16
16
|
todos: TodosService;
|
|
17
17
|
todoUi: TodoUiSlot;
|
|
18
18
|
mirrorTodos: () => void;
|
|
19
|
+
/** Master-only stop reminder; omit on worker panes. */
|
|
20
|
+
stopReminder?: {
|
|
21
|
+
getBlockedDepth: () => number;
|
|
22
|
+
getRunningSubs: () => number;
|
|
23
|
+
};
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
export async function
|
|
26
|
+
export async function mountTodoOnly(m: TodoOnlyMount): Promise<void> {
|
|
22
27
|
const workerRoot = new Context();
|
|
23
28
|
const workerSurface = new PiSurface(m.pi as unknown as object);
|
|
24
29
|
workerRoot.provide('pi-herdr.surface', workerSurface);
|
|
@@ -31,6 +36,7 @@ export async function mountWorkerTodo(m: WorkerPluginMount): Promise<void> {
|
|
|
31
36
|
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
32
37
|
},
|
|
33
38
|
state: m.todoUi,
|
|
39
|
+
...(m.stopReminder ? { stopReminder: m.stopReminder } : {}),
|
|
34
40
|
});
|
|
35
41
|
await workerRoot.plugin(todoPlugin);
|
|
36
42
|
m.pi.on('session_shutdown', () => {
|
package/src/index.ts
CHANGED
|
@@ -7,10 +7,7 @@
|
|
|
7
7
|
* Capabilities:
|
|
8
8
|
* - `todo_write`: replace the complete todo list; the current pi session JSONL is authoritative,
|
|
9
9
|
* and branches automatically roll back in DSH-aligned semantics.
|
|
10
|
-
* - `subagent
|
|
11
|
-
* with an independent session/context that humans can enter directly; the controller uses
|
|
12
|
-
* herdr for state-gated injection (idle only), waiting (agent.wait), and JSONL result reads.
|
|
13
|
-
* - `list_agents` / `send_message` (followUp queue semantics) / `interrupt_agent` (esc).
|
|
10
|
+
* - `subagent` / `terminal`: herdr-master only. Bare pi does not register them.
|
|
14
11
|
* - `/todos`, TUI widget, and herdr title projection with graceful degradation without herdr.
|
|
15
12
|
*
|
|
16
13
|
* pi 0.84.2 contract (validated):
|
|
@@ -111,9 +108,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
111
108
|
|
|
112
109
|
/** Tier 2: worker role badge parsed from the env manifest and kept visible while idle. */
|
|
113
110
|
let roleBadge: string | null = null;
|
|
111
|
+
/** Human-gate depth; ask_user_question and external herdr:blocked events share it. */
|
|
112
|
+
let blockedDepth = 0;
|
|
114
113
|
|
|
115
114
|
function reportAgent(state: 'working' | 'idle' | 'blocked', activity: string | null): void {
|
|
116
115
|
if (!client.available) return;
|
|
116
|
+
// While a human gate is open, never let tool-badge / turn_start / settle overwrite blocked.
|
|
117
|
+
// Needed when herdr:pi is absent (pi-herdr is the authority); no-op under full-lifecycle herdr:pi.
|
|
118
|
+
if (blockedDepth > 0 && state !== 'blocked') return;
|
|
117
119
|
const message = activity ?? (state === 'idle' ? roleBadge : null);
|
|
118
120
|
client.reportAgent(state, message).catch(() => {});
|
|
119
121
|
}
|
|
@@ -156,8 +158,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
156
158
|
progressSuffix: suffix,
|
|
157
159
|
lastWriteAt: todos.lastWriteAt,
|
|
158
160
|
});
|
|
159
|
-
// D97:
|
|
160
|
-
updateSlimFrame(title);
|
|
161
|
+
// D97: overlay content shares the todo snapshot; thinking tokens cannot reach it.
|
|
162
|
+
updateSlimFrame({ title, items: todos.items, lastWriteAt: todos.lastWriteAt });
|
|
161
163
|
client.reportMetadata({
|
|
162
164
|
session: label,
|
|
163
165
|
items: todos.items,
|
|
@@ -226,11 +228,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
226
228
|
}
|
|
227
229
|
});
|
|
228
230
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
if (env) {
|
|
232
|
+
installWriteLocks(pi, {
|
|
233
|
+
client,
|
|
234
|
+
env,
|
|
235
|
+
hard: process.env[WRITE_LOCK_ENV] === '1',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
234
238
|
|
|
235
239
|
/* ── todo family slot (core/todo.ts fills the plugin hook; widget rendering moved with the family) ── */
|
|
236
240
|
const todoUi: { renderWidget: (ctx: unknown) => void } = {
|
|
@@ -369,15 +373,16 @@ export default async function (pi: ExtensionAPI) {
|
|
|
369
373
|
if (plan.notice && !isSubagent) {
|
|
370
374
|
const running = subagentPort.current?.listRunningSubs() ?? [];
|
|
371
375
|
const brief = running.map((s) => `${s.paneId} (${s.description})`).join('、');
|
|
372
|
-
const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(
|
|
376
|
+
const notice = `注意:仍有 ${running.length} 个后台 subagent 在运行:${brief}。若你的任务依赖它们,请等待其结算(subagent list 查看状态);若不等待,请说明放弃原因。`;
|
|
373
377
|
void sendUserMessageIn(notice);
|
|
374
378
|
}
|
|
375
379
|
});
|
|
376
380
|
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
+
// Official herdr:pi is full-lifecycle authority when installed: it reports blocked
|
|
382
|
+
// only on the herdr:blocked event (pi core never emits it). Custom source=pi-herdr
|
|
383
|
+
// pane.report_agent is ignored while that authority is live
|
|
384
|
+
// (screen_detection_skip_reason=full_lifecycle_hook_authority). Emit the event so
|
|
385
|
+
// herdr:pi publishes blocked; keep report_agent as fallback when it is absent.
|
|
381
386
|
function enterBlocked(label: string | null): void {
|
|
382
387
|
blockedDepth += 1;
|
|
383
388
|
reportAgent('blocked', label);
|
|
@@ -392,14 +397,34 @@ export default async function (pi: ExtensionAPI) {
|
|
|
392
397
|
void client.reportAskFlag(null).catch(() => {});
|
|
393
398
|
}
|
|
394
399
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
+
|
|
401
|
+
// emit() is synchronous; this flag stops our own listener from double-counting depth.
|
|
402
|
+
let publishingHerdrBlocked = false;
|
|
403
|
+
function publishHerdrBlocked(active: boolean, label: string | null): void {
|
|
404
|
+
publishingHerdrBlocked = true;
|
|
405
|
+
try {
|
|
406
|
+
if (active) enterBlocked(label);
|
|
400
407
|
else exitBlocked();
|
|
401
|
-
|
|
402
|
-
|
|
408
|
+
pi.events.emit(
|
|
409
|
+
'herdr:blocked',
|
|
410
|
+
active ? { active: true, ...(label ? { label } : {}) } : { active: false },
|
|
411
|
+
);
|
|
412
|
+
} finally {
|
|
413
|
+
publishingHerdrBlocked = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
pi.events.on('herdr:blocked', (data) => {
|
|
417
|
+
if (publishingHerdrBlocked) return;
|
|
418
|
+
if (!data || typeof data !== 'object') {
|
|
419
|
+
exitBlocked();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!('active' in data) || data.active !== true) {
|
|
423
|
+
exitBlocked();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
enterBlocked('label' in data && typeof data.label === 'string' ? data.label : null);
|
|
427
|
+
});
|
|
403
428
|
|
|
404
429
|
/* ── Tool: ask_user_question (v1.3 M8 human gate, available to master and subagents) ── */
|
|
405
430
|
|
|
@@ -423,9 +448,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
423
448
|
const ui = (ctx as {
|
|
424
449
|
ui?: { input?: (title: string, placeholder?: string) => Promise<string | undefined> };
|
|
425
450
|
}).ui;
|
|
426
|
-
|
|
427
|
-
//
|
|
428
|
-
//
|
|
451
|
+
publishHerdrBlocked(true, question);
|
|
452
|
+
// Fallback when herdr:pi is absent: a one-shot pi-herdr blocked report can lose
|
|
453
|
+
// to later working reports; refresh while the gate is open. Do not re-emit
|
|
454
|
+
// herdr:blocked here — official blockedCount is edge-triggered.
|
|
429
455
|
const hb = setInterval(() => { reportAgent('blocked', question); }, 5000);
|
|
430
456
|
try {
|
|
431
457
|
const answer = await ui?.input?.(question, 'your answer');
|
|
@@ -435,7 +461,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
435
461
|
};
|
|
436
462
|
} finally {
|
|
437
463
|
clearInterval(hb);
|
|
438
|
-
|
|
464
|
+
publishHerdrBlocked(false, null);
|
|
439
465
|
}
|
|
440
466
|
},
|
|
441
467
|
});
|
|
@@ -533,8 +559,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
533
559
|
}
|
|
534
560
|
});
|
|
535
561
|
|
|
536
|
-
/*
|
|
537
|
-
|
|
562
|
+
/* Workbench plugins (subagent/terminal + cordis) are herdr-master only.
|
|
563
|
+
* Bare pi and worker panes mount todo only so unused tools never appear.
|
|
564
|
+
* Dynamic import: worker/bare-pi must never load bootstrap/subagent-scope (C3). */
|
|
565
|
+
if (mode.composeMaster) {
|
|
538
566
|
const { mountMasterPlugins } = await import('./index-master.ts');
|
|
539
567
|
await mountMasterPlugins({
|
|
540
568
|
pi,
|
|
@@ -555,7 +583,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
555
583
|
claimSettleNotice,
|
|
556
584
|
});
|
|
557
585
|
} else {
|
|
558
|
-
const {
|
|
559
|
-
await
|
|
586
|
+
const { mountTodoOnly } = await import('./index-worker.ts');
|
|
587
|
+
await mountTodoOnly({
|
|
588
|
+
pi,
|
|
589
|
+
todos,
|
|
590
|
+
todoUi,
|
|
591
|
+
mirrorTodos,
|
|
592
|
+
...(isSubagent
|
|
593
|
+
? {}
|
|
594
|
+
: {
|
|
595
|
+
stopReminder: {
|
|
596
|
+
getBlockedDepth: () => blockedDepth,
|
|
597
|
+
getRunningSubs: () => subagentPort.current?.listRunningSubs().length ?? 0,
|
|
598
|
+
},
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
560
601
|
}
|
|
561
602
|
}
|
package/src/notice-buffer.ts
CHANGED
|
@@ -12,12 +12,12 @@ export const NOTICE_MAX_SHOWN = 3;
|
|
|
12
12
|
* Collapse a batch into one message body.
|
|
13
13
|
* - empty → null (do not inject)
|
|
14
14
|
* - ≤max → originals joined with `\n\n` (single-item byte-identical to the old format)
|
|
15
|
-
* - >max → first max originals + a tail pointing at history /
|
|
15
|
+
* - >max → first max originals + a tail pointing at history / subagent list
|
|
16
16
|
*/
|
|
17
17
|
export function collapseNotices(contents: readonly string[], max = NOTICE_MAX_SHOWN): string | null {
|
|
18
18
|
if (contents.length === 0) return null;
|
|
19
19
|
if (contents.length <= max) return contents.join('\n\n');
|
|
20
20
|
const hidden = contents.length - max;
|
|
21
|
-
const tail = `…另有 ${hidden} 条结算未逐条展示。全量结果看 history 台账(路径公式见
|
|
21
|
+
const tail = `…另有 ${hidden} 条结算未逐条展示。全量结果看 history 台账(路径公式见 subagent resume 工具描述);在跑代理用 subagent list 查看。`;
|
|
22
22
|
return [...contents.slice(0, max), tail].join('\n\n');
|
|
23
23
|
}
|
package/src/roles/master.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"role": "master",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "主控角色(WS-D7 自应用;D83
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "主控角色(WS-D7 自应用;D83 镜像源):核心工具(todos/协调/subagent/terminal)+ pi 内建 + web_search;D82 unknownTools=allow。v1.4.0: 工具合并 terminal 6→1, subagent 5→1",
|
|
5
5
|
"manifest": {
|
|
6
6
|
"unknownTools": "allow",
|
|
7
7
|
"tools": [
|
|
8
8
|
"todo_write", "ask_user_question",
|
|
9
|
-
"subagent",
|
|
10
|
-
"
|
|
9
|
+
"subagent",
|
|
10
|
+
"terminal",
|
|
11
11
|
"read", "grep", "glob", "bash", "pwsh", "edit", "write",
|
|
12
12
|
"web_search"
|
|
13
13
|
],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"role": "worker-default",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "默认worker角色(D83 继承制):镜像 master.tools 减 subagent
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "默认worker角色(D83 继承制):镜像 master.tools 减 subagent/terminal(禁嵌套、资源归属);排除族显式 deny——在 D82 unknownTools=allow 姿态下仍持禁令;todos serial。v1.4.0: 工具合并后更新 deny 列表",
|
|
5
5
|
"manifest": {
|
|
6
6
|
"unknownTools": "allow",
|
|
7
7
|
"tools": [
|
|
@@ -11,16 +11,7 @@
|
|
|
11
11
|
],
|
|
12
12
|
"rules": {
|
|
13
13
|
"subagent": "deny",
|
|
14
|
-
"
|
|
15
|
-
"list_agents": "deny",
|
|
16
|
-
"send_message": "deny",
|
|
17
|
-
"interrupt_agent": "deny",
|
|
18
|
-
"terminal_open": "deny",
|
|
19
|
-
"terminal_send": "deny",
|
|
20
|
-
"terminal_read": "deny",
|
|
21
|
-
"terminal_signal": "deny",
|
|
22
|
-
"terminal_close": "deny",
|
|
23
|
-
"terminal_list": "deny",
|
|
14
|
+
"terminal": "deny",
|
|
24
15
|
"*": "allow"
|
|
25
16
|
}
|
|
26
17
|
},
|