pi-pier 0.3.0 → 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 +16 -12
- package/package.json +3 -3
- package/schemas/role-manifest.schema.json +7 -1
- package/skills/pi-herdr-workflow/SKILL.md +4 -5
- package/src/ansi-text.ts +62 -11
- package/src/ask-dialog.ts +308 -0
- package/src/ask-user.ts +113 -112
- package/src/bootstrap.ts +10 -13
- package/src/compact-coordinator.ts +279 -261
- package/src/compact-economics-core.ts +77 -145
- package/src/config-catalog-core.ts +77 -197
- package/src/config-command.ts +271 -64
- package/src/cordis-services.ts +64 -0
- package/src/dashboard-command.ts +77 -33
- package/src/delegation-core.ts +115 -0
- package/src/efficiency-config-core.ts +286 -486
- package/src/efficiency-store.ts +16 -29
- package/src/gc-core.ts +10 -36
- package/src/guards.ts +6 -0
- package/src/herdr-client.ts +202 -233
- package/src/history-store.ts +38 -23
- package/src/index-gates.ts +118 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +31 -29
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +31 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +16 -6
- package/src/index-worker.ts +8 -18
- package/src/index.ts +561 -570
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +138 -90
- package/src/ledger.ts +13 -21
- package/src/lock-core.ts +28 -56
- package/src/manifest-compose.ts +35 -20
- package/src/observation-core.ts +52 -100
- package/src/pane-title.ts +19 -38
- package/src/panel-open.ts +47 -0
- package/src/pi-surface.ts +34 -22
- package/src/pier-options.ts +53 -50
- package/src/pipe-channel.ts +44 -59
- package/src/platform-paths.ts +19 -52
- package/src/{core → plugins}/grid-shape.ts +8 -10
- package/src/{core → plugins}/heat-plan.ts +20 -22
- package/src/plugins/observation.ts +475 -0
- package/src/plugins/subagent.ts +766 -0
- package/src/{core → plugins}/terminal.ts +198 -139
- package/src/{core → plugins}/todo.ts +73 -134
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +26 -64
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +108 -84
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +25 -31
- package/src/role-state.ts +119 -0
- package/src/roles/worker-default.json +3 -3
- package/src/routing-telemetry.ts +136 -0
- package/src/runtime-policy.ts +25 -40
- package/src/session-tail.ts +65 -41
- package/src/settle-wake-core.ts +61 -22
- package/src/slim-frame.ts +25 -122
- package/src/stale-core.ts +17 -24
- package/src/storage-layout.ts +14 -43
- package/src/subagent-core.ts +423 -120
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +383 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +577 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +187 -134
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +46 -39
- package/src/todo-reminder-core.ts +26 -51
- package/src/todo-window.ts +11 -12
- package/src/todos-service.ts +15 -35
- package/src/tool-error.ts +6 -13
- package/src/tool-gate.ts +38 -21
- package/src/tree-view.ts +162 -0
- package/src/vocab.ts +52 -25
- package/src/worker-marker.ts +76 -0
- package/src/ask-multi.ts +0 -260
- package/src/config-guide.ts +0 -361
- package/src/core/observation.ts +0 -555
- package/src/core/subagent.ts +0 -633
- package/src/efficiency-config-io.ts +0 -53
- package/src/focus-poller.ts +0 -244
- package/src/git-adapter.ts +0 -113
- package/src/notice-buffer.ts +0 -23
- package/src/subagent-execute.ts +0 -328
- package/src/subagent-git-io.ts +0 -80
- package/src/subagent-launch.ts +0 -114
- package/src/subagent-list-action.ts +0 -69
- package/src/subagent-output-action.ts +0 -104
- package/src/subagent-output-adapter.ts +0 -27
- package/src/subagent-output-core.ts +0 -263
- package/src/subagent-poll-loop.ts +0 -305
- package/src/subagent-port.ts +0 -27
- package/src/subagent-ready.ts +0 -89
- package/src/subagent-registry.ts +0 -70
- package/src/subagent-resolution.ts +0 -41
- package/src/subagent-scope.ts +0 -41
- package/src/subagent-session-io.ts +0 -230
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role runtime for the composition root: manifest state, mid-session switching,
|
|
3
|
+
* the mandatory tool gate, `/pier-role`, and branch replay.
|
|
4
|
+
*/
|
|
5
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { composeForRole, toRuntimeManifest } from './manifest-compose.ts';
|
|
9
|
+
import { planActiveTools, planToolGate, type RuntimeRoleManifest } from './tool-gate.ts';
|
|
10
|
+
import {
|
|
11
|
+
initialRoleState,
|
|
12
|
+
latestRoleManifestRecord,
|
|
13
|
+
manifestFromRecord,
|
|
14
|
+
planRoleSwitch,
|
|
15
|
+
planSwitchActiveTools,
|
|
16
|
+
roleRecordDiffers,
|
|
17
|
+
type RoleState,
|
|
18
|
+
} from './role-state.ts';
|
|
19
|
+
import { RESERVED_ROLE_NAMES, listRoleNames, roleLayers } from './role-loader.ts';
|
|
20
|
+
import { TodosService } from './todos-service.ts';
|
|
21
|
+
import { APPROVAL_NEEDED_CUSTOM_TYPE, ROLE_MANIFEST_CUSTOM_TYPE } from './renderers.ts';
|
|
22
|
+
import { planDenyHitRow, scanRoleAxisUsage, type RoutingTelemetryRecord } from './routing-telemetry.ts';
|
|
23
|
+
import type { HerdrClientLike } from './herdr-client.ts';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* WS-D7: the master pane applies its own manifest through the same mandatory chain as
|
|
27
|
+
* subagents. The builtin `master` role is read directly so a workspace master.json decoy
|
|
28
|
+
* cannot affect self-application; a malformed manifest fails open to no-role state.
|
|
29
|
+
*/
|
|
30
|
+
export function composeMasterRuntime(): RuntimeRoleManifest | null {
|
|
31
|
+
try {
|
|
32
|
+
return toRuntimeManifest(composeForRole('master', [], { loadRoleOpts: { builtinDirect: true } }));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`[pi-herdr] master manifest invalid (fail-open): ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RoleRuntimeDeps {
|
|
40
|
+
pi: ExtensionAPI;
|
|
41
|
+
/** D93 sidebar identity (`display_agent`) is a herdr RPC — pi's ExtensionAPI has no such method. */
|
|
42
|
+
client: HerdrClientLike;
|
|
43
|
+
/** Role-resolution base dir carried by the master at spawn (an isolate worker's own cwd resolves a different checkout). */
|
|
44
|
+
roleBase: string;
|
|
45
|
+
initialManifest: RuntimeRoleManifest | null;
|
|
46
|
+
isSubagent: boolean;
|
|
47
|
+
todos: TodosService;
|
|
48
|
+
/** Best-effort routing telemetry (RFC docs/rfc-jev-role-routing.md §8); observation only, never gates. */
|
|
49
|
+
appendRoutingLog: (row: RoutingTelemetryRecord) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RoleRuntime {
|
|
53
|
+
readonly state: RoleState;
|
|
54
|
+
idleBadge(): string | null;
|
|
55
|
+
/** Replay the branch-point role record and re-anchor side effects (D77 visible layer, sidebar identity, badge). */
|
|
56
|
+
syncFromBranch(ctx: unknown): void;
|
|
57
|
+
/** Phase 0 axis scan of the workspace+user role layers (master session_start only). */
|
|
58
|
+
scanRoleUsage(): void;
|
|
59
|
+
/** Mid-session switch: /pier-role (human) and the pipe `role` request (master). */
|
|
60
|
+
applyRoleSwitch(roleName: string, switchedBy: string): Promise<{ ok: boolean; message: string }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createRoleRuntime(d: RoleRuntimeDeps): RoleRuntime {
|
|
64
|
+
const { pi, client, todos, roleBase, appendRoutingLog } = d;
|
|
65
|
+
const roleState = initialRoleState(d.initialManifest);
|
|
66
|
+
let roleBadge: string | null = null;
|
|
67
|
+
|
|
68
|
+
/** D93: sidebar identity is the role name; null clears it. Best effort — herdr may be absent. */
|
|
69
|
+
const reportSidebarIdentity = (role: string): void => {
|
|
70
|
+
void client.reportDisplayAgent(role === 'worker-default' ? 'worker' : role).catch(() => {});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const setBadge = (m: RuntimeRoleManifest): string =>
|
|
74
|
+
(roleBadge = `role ${m.role} v${m.version ?? '?'} (${m.tools.length} tools)`);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Same-origin sync (RFC docs/rfc-pi-0.86-dynamic-tools.md §4.3): reconcile roleState against the CURRENT
|
|
78
|
+
* branch point on session_start (resume) and session_tree — pi's tool deltas revert the loadout at the
|
|
79
|
+
* branch point, so the gate manifest must follow or it would keep the post-switch tools. Replay only
|
|
80
|
+
* when the last record names a DIFFERENT role (name equality tolerates on-disk drift and keeps the env
|
|
81
|
+
* value authoritative on resume); the anchor entry is written only on change so resume replay still works. */
|
|
82
|
+
function syncFromBranch(ctx: unknown): void {
|
|
83
|
+
if (!roleState.manifest) return;
|
|
84
|
+
try {
|
|
85
|
+
const branchEntries =
|
|
86
|
+
(ctx as { sessionManager?: { getBranch?: () => readonly unknown[] } }).sessionManager?.getBranch?.() ?? [];
|
|
87
|
+
const record = latestRoleManifestRecord(branchEntries);
|
|
88
|
+
if (record && record.role !== roleState.manifest.role) {
|
|
89
|
+
roleState.manifest = manifestFromRecord(record);
|
|
90
|
+
roleState.origin = record.origin === 'switch' ? 'switch' : 'env';
|
|
91
|
+
roleState.switchedBy = record.switchedBy ?? null;
|
|
92
|
+
roleState.switchedAt = record.ts ?? null;
|
|
93
|
+
console.error(`[pi-herdr] role replay: ${record.role} (origin ${record.origin ?? 'env'}, switchedBy ${record.switchedBy ?? '?'})`);
|
|
94
|
+
}
|
|
95
|
+
if (roleRecordDiffers(record, roleState)) {
|
|
96
|
+
const m = roleState.manifest;
|
|
97
|
+
pi.appendEntry(ROLE_MANIFEST_CUSTOM_TYPE, {
|
|
98
|
+
version: 1,
|
|
99
|
+
role: m.role,
|
|
100
|
+
manifestVersion: m.version,
|
|
101
|
+
tools: m.tools,
|
|
102
|
+
permissions: m.permissions,
|
|
103
|
+
unknownTools: m.unknownTools ?? 'deny',
|
|
104
|
+
...(m.guidelines?.length ? { guidelines: m.guidelines } : {}),
|
|
105
|
+
...(roleState.origin === 'switch'
|
|
106
|
+
? { origin: 'switch' as const, switchedBy: roleState.switchedBy ?? undefined }
|
|
107
|
+
: {}),
|
|
108
|
+
ts: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
/* Best effort recording; the mandatory layer does not depend on the session log. */
|
|
113
|
+
}
|
|
114
|
+
reportSidebarIdentity(roleState.manifest.role);
|
|
115
|
+
setBadge(roleState.manifest);
|
|
116
|
+
// D77 visible layer: hide tools outside the manifest AFTER all plugins load.
|
|
117
|
+
// Intersection semantics prevent clearing everything; master without a
|
|
118
|
+
// manifest stays full. setActiveTools persists as a tool delta (pi 0.86).
|
|
119
|
+
try {
|
|
120
|
+
const active = pi.getActiveTools();
|
|
121
|
+
const vis = planActiveTools(roleState.manifest.tools, active, {
|
|
122
|
+
unknownTools: roleState.manifest.unknownTools,
|
|
123
|
+
permissions: roleState.manifest.permissions,
|
|
124
|
+
});
|
|
125
|
+
if (vis && vis.changed) {
|
|
126
|
+
pi.setActiveTools(vis.next);
|
|
127
|
+
console.error(`[pi-herdr] D77 visible-layer: role ${roleState.manifest.role} tools ${active.length} → ${vis.next.length}`);
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
/* Best effort visible layer; the mandatory layer remains active. */
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Axis scan reads file TEXT of the two top layers; per-layer readdir is guarded (workspace layer is commonly absent). */
|
|
135
|
+
function scanRoleUsage(): void {
|
|
136
|
+
try {
|
|
137
|
+
const files: Array<{ name: string; text: string }> = [];
|
|
138
|
+
for (const layer of roleLayers({ baseDir: roleBase }).slice(0, 2)) {
|
|
139
|
+
let names: string[];
|
|
140
|
+
try {
|
|
141
|
+
names = readdirSync(layer.dir);
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
for (const f of names) {
|
|
146
|
+
if (!f.endsWith('.json')) continue;
|
|
147
|
+
try {
|
|
148
|
+
files.push({ name: f, text: readFileSync(join(layer.dir, f), 'utf8') });
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
appendRoutingLog(scanRoleAxisUsage({ now: Date.now(), files }));
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function applyRoleSwitch(
|
|
159
|
+
roleName: string,
|
|
160
|
+
switchedBy: string,
|
|
161
|
+
): Promise<{ ok: boolean; message: string }> {
|
|
162
|
+
const current = roleState.manifest;
|
|
163
|
+
if (!current) {
|
|
164
|
+
return { ok: false, message: 'Error: role system not armed (no manifest; bare pi sessions cannot switch roles)' };
|
|
165
|
+
}
|
|
166
|
+
const name = roleName.trim();
|
|
167
|
+
if (name === '') return { ok: false, message: 'Error: role name is required (see /pier-role with no argument)' };
|
|
168
|
+
if (name === current.role) return { ok: true, message: `role is already ${name}` };
|
|
169
|
+
let next: RuntimeRoleManifest;
|
|
170
|
+
try {
|
|
171
|
+
// Resolve against the spawn-inherited base, matching the initial manifest composition.
|
|
172
|
+
next = toRuntimeManifest(composeForRole(name, [], { loadRoleOpts: { baseDir: roleBase } }));
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { ok: false, message: `Error: role "${name}" unavailable: ${(err as Error).message}` };
|
|
175
|
+
}
|
|
176
|
+
const plan = planRoleSwitch(current.tools, next.tools);
|
|
177
|
+
// Universe = ALL registered tools: a switch may re-admit tools an earlier
|
|
178
|
+
// prune removed, which intersection semantics would lose.
|
|
179
|
+
const registered = pi.getAllTools().map((t) => t.name);
|
|
180
|
+
const nextActive = planSwitchActiveTools(next.tools, registered, {
|
|
181
|
+
unknownTools: next.unknownTools,
|
|
182
|
+
permissions: next.permissions,
|
|
183
|
+
});
|
|
184
|
+
pi.setActiveTools(nextActive);
|
|
185
|
+
// TodosService captured services.todos.mode at startup; a mode switch must reach it.
|
|
186
|
+
todos.config.allowParallelInProgress = TodosService.configFromRuntime(next, d.isSubagent).allowParallelInProgress;
|
|
187
|
+
roleState.manifest = next;
|
|
188
|
+
roleState.origin = 'switch';
|
|
189
|
+
roleState.switchedBy = switchedBy;
|
|
190
|
+
roleState.switchedAt = Date.now();
|
|
191
|
+
setBadge(next);
|
|
192
|
+
try {
|
|
193
|
+
pi.appendEntry(ROLE_MANIFEST_CUSTOM_TYPE, {
|
|
194
|
+
version: 1,
|
|
195
|
+
role: next.role,
|
|
196
|
+
manifestVersion: next.version,
|
|
197
|
+
tools: next.tools,
|
|
198
|
+
permissions: next.permissions,
|
|
199
|
+
unknownTools: next.unknownTools ?? 'deny',
|
|
200
|
+
...(next.guidelines?.length ? { guidelines: next.guidelines } : {}),
|
|
201
|
+
origin: 'switch' as const,
|
|
202
|
+
switchedBy,
|
|
203
|
+
ts: Date.now(),
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
reportSidebarIdentity(next.role);
|
|
208
|
+
const diff = [
|
|
209
|
+
plan.added.length ? `+${plan.added.join(', ')}` : null,
|
|
210
|
+
plan.removed.length ? `-${plan.removed.join(', ')}` : null,
|
|
211
|
+
]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join(' ');
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
message: `role ${current.role} → ${next.role}${diff ? ` (${diff})` : ''}; active tools: ${nextActive.length}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Mandatory gate: outside-manifest tool → block + terminate the batch;
|
|
221
|
+
* ask → v1 soft-allow with a durable APPROVAL_NEEDED trace (V56). */
|
|
222
|
+
pi.on('tool_call', async (event: { toolName?: string }) => {
|
|
223
|
+
const manifest = roleState.manifest;
|
|
224
|
+
if (!manifest) return;
|
|
225
|
+
const tool = typeof event?.toolName === 'string' ? event.toolName : '';
|
|
226
|
+
const gate = planToolGate(tool, manifest);
|
|
227
|
+
if (gate.kind === 'deny') {
|
|
228
|
+
appendRoutingLog(planDenyHitRow({ now: Date.now(), role: manifest.role, tool }));
|
|
229
|
+
return { block: true, reason: gate.reason, terminate: true };
|
|
230
|
+
}
|
|
231
|
+
if (gate.kind === 'ask') {
|
|
232
|
+
console.error(`${gate.notice} (v1 soft-approval: allowed, hard gate lands in v2)`);
|
|
233
|
+
try {
|
|
234
|
+
pi.appendEntry(APPROVAL_NEEDED_CUSTOM_TYPE, { role: manifest.role, tool, ts: Date.now() });
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
pi.registerCommand('pier-role', {
|
|
242
|
+
description:
|
|
243
|
+
'Show the current role + available role names, or switch: /pier-role <name> (widening beyond the current toolset asks for confirmation)',
|
|
244
|
+
handler: async (args: unknown, ctx: unknown) => {
|
|
245
|
+
const ui = (ctx as { ui?: { notify?: (t: string, l?: string) => void; confirm?: (t: string, m: string) => Promise<boolean> } }).ui;
|
|
246
|
+
const notify = (text: string, level: 'info' | 'error') => ui?.notify?.(text, level) ?? console.error(text);
|
|
247
|
+
const current = roleState.manifest;
|
|
248
|
+
if (!current) {
|
|
249
|
+
notify('Role system not armed (no manifest).', 'error');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const name = typeof args === 'string' ? args.trim() : '';
|
|
253
|
+
if (name === '') {
|
|
254
|
+
const names = new Set<string>(RESERVED_ROLE_NAMES);
|
|
255
|
+
for (const n of listRoleNames(roleBase)) names.add(n);
|
|
256
|
+
notify(
|
|
257
|
+
`Current role: ${current.role} (origin: ${roleState.origin}). Available: ${[...names].sort().join(', ')}`,
|
|
258
|
+
'info',
|
|
259
|
+
);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const preview = toRuntimeManifest(composeForRole(name, [], { loadRoleOpts: { baseDir: roleBase } }));
|
|
264
|
+
const plan = planRoleSwitch(current.tools, preview.tools);
|
|
265
|
+
// Q2: humans may widen after confirm — the human is the final authority in the pane.
|
|
266
|
+
if (plan.widening && ui?.confirm) {
|
|
267
|
+
const okToWiden = await ui.confirm(
|
|
268
|
+
'Widen role toolset',
|
|
269
|
+
`Role "${name}" adds tools beyond the current set (${plan.added.join(', ')}). Switch anyway?`,
|
|
270
|
+
);
|
|
271
|
+
if (!okToWiden) {
|
|
272
|
+
notify('Role switch cancelled.', 'info');
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} catch (err) {
|
|
277
|
+
notify(`Error: role "${name}" unavailable: ${(err as Error).message}`, 'error');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const res = await applyRoleSwitch(name, 'human');
|
|
281
|
+
notify(res.message, res.ok ? 'info' : 'error');
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/* Per-role guidelines ride the before_agent_start prompt-section diff; writing
|
|
286
|
+
* the same string each turn is a no-op (pi diffs sections), removal cleans up. */
|
|
287
|
+
pi.on('before_agent_start', async (event) => {
|
|
288
|
+
const sections = event.systemPromptOptions?.sections;
|
|
289
|
+
if (!sections) return;
|
|
290
|
+
const m = roleState.manifest;
|
|
291
|
+
if (m?.guidelines?.length) {
|
|
292
|
+
const text = `Role "${m.role}" operating constraints:\n${m.guidelines.map((g) => `- ${g}`).join('\n')}`;
|
|
293
|
+
if (sections['pier-role'] !== text) sections['pier-role'] = text;
|
|
294
|
+
} else {
|
|
295
|
+
delete sections['pier-role'];
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
state: roleState,
|
|
301
|
+
idleBadge: () => roleBadge,
|
|
302
|
+
syncFromBranch,
|
|
303
|
+
scanRoleUsage,
|
|
304
|
+
applyRoleSwitch,
|
|
305
|
+
};
|
|
306
|
+
}
|
package/src/index-runtime.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Index process-mode planner (master vs worker, master-manifest gate)
|
|
3
|
-
*
|
|
4
|
-
* Why: index.ts mixed env branching with cordis/plugin mounting. The mode
|
|
5
|
-
* decision is a pure function of process env and must stay testable without
|
|
6
|
-
* booting the composition root.
|
|
7
|
-
*/
|
|
2
|
+
* Index process-mode planner (master vs worker, master-manifest gate): a pure function of
|
|
3
|
+
* process env that must stay testable without booting the composition root. */
|
|
8
4
|
import { detectHerdrEnv } from './herdr-client.ts';
|
|
9
5
|
|
|
10
6
|
export interface IndexMode {
|
|
@@ -25,3 +21,17 @@ export function planIndexMode(env: NodeJS.ProcessEnv = process.env): IndexMode {
|
|
|
25
21
|
composeMaster: hasHerdr && !isSubagent,
|
|
26
22
|
};
|
|
27
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* herdr 0.9.2 restore path: a reopened worker pane runs pier's resume_argv, which cannot carry the
|
|
27
|
+
* spawn env. A worker marker next to the session file is the durable identity, so the restored boot
|
|
28
|
+
* plans worker mode (todo-only mount, no master services) instead of master composition.
|
|
29
|
+
*/
|
|
30
|
+
export function applyRestoredWorkerMode(
|
|
31
|
+
mode: IndexMode,
|
|
32
|
+
sessionFile: string | null | undefined,
|
|
33
|
+
markerExists: (sessionFile: string) => boolean,
|
|
34
|
+
): IndexMode {
|
|
35
|
+
if (mode.isSubagent || !mode.hasHerdr || !sessionFile) return mode;
|
|
36
|
+
return markerExists(sessionFile) ? { isSubagent: true, hasHerdr: true, composeMaster: false } : mode;
|
|
37
|
+
}
|
package/src/index-worker.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Used by worker panes and by standalone pi outside herdr, so bootstrap/subagent/terminal
|
|
5
|
-
* stay out of those processes.
|
|
6
|
-
*/
|
|
1
|
+
/** Todo-only plugin mount (bare cordis root, no loader/hmr). Used by worker panes and by standalone
|
|
2
|
+
* pi outside herdr, so bootstrap/subagent/terminal stay out of those processes. */
|
|
7
3
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
8
4
|
import { Context } from '@deepseek-ai/cordis';
|
|
9
5
|
import { PiSurface } from './pi-surface.ts';
|
|
10
|
-
import
|
|
6
|
+
import { baseTodoDeps, cordisProvide } from './cordis-services.ts';
|
|
7
|
+
import todoPlugin from './plugins/todo.ts';
|
|
11
8
|
import type { TodosService } from './todos-service.ts';
|
|
12
|
-
import type { TodoUiSlot } from './
|
|
9
|
+
import type { TodoUiSlot } from './plugins/todo.ts';
|
|
13
10
|
|
|
14
11
|
export interface TodoOnlyMount {
|
|
15
12
|
pi: ExtensionAPI;
|
|
@@ -30,16 +27,9 @@ export interface TodoOnlyMount {
|
|
|
30
27
|
export async function mountTodoOnly(m: TodoOnlyMount): Promise<void> {
|
|
31
28
|
const workerRoot = new Context();
|
|
32
29
|
const workerSurface = new PiSurface(m.pi as unknown as object);
|
|
33
|
-
workerRoot
|
|
34
|
-
workerRoot
|
|
35
|
-
|
|
36
|
-
allowParallelInProgress: m.todos.config.allowParallelInProgress,
|
|
37
|
-
maxItems: 15,
|
|
38
|
-
mirrorTodos: m.mirrorTodos,
|
|
39
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
40
|
-
(m.pi as { appendEntry?: (t: string, d: unknown) => void }).appendEntry?.(customType, data);
|
|
41
|
-
},
|
|
42
|
-
state: m.todoUi,
|
|
30
|
+
cordisProvide(workerRoot, 'pi-herdr.surface', workerSurface);
|
|
31
|
+
cordisProvide(workerRoot, 'pi-herdr.todo-deps', {
|
|
32
|
+
...baseTodoDeps(m),
|
|
43
33
|
...(m.getBlockedDepth ? { getBlockedDepth: m.getBlockedDepth } : {}),
|
|
44
34
|
...(m.stopReminder ? { stopReminder: m.stopReminder } : {}),
|
|
45
35
|
});
|