pi-pier 0.3.0 → 0.3.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 +12 -11
- package/package.json +2 -2
- 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 +8 -12
- package/src/compact-coordinator.ts +278 -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/dashboard-command.ts +38 -19
- package/src/efficiency-config-core.ts +287 -484
- package/src/efficiency-store.ts +16 -29
- package/src/focus-poller.ts +14 -35
- package/src/gc-core.ts +10 -36
- package/src/herdr-client.ts +82 -208
- package/src/history-store.ts +36 -23
- package/src/index-gates.ts +124 -0
- package/src/index-locks.ts +2 -7
- package/src/index-master.ts +24 -18
- package/src/index-notices.ts +28 -15
- package/src/index-pipe.ts +17 -12
- package/src/index-roles.ts +306 -0
- package/src/index-runtime.ts +2 -6
- package/src/index-worker.ts +4 -8
- package/src/index.ts +305 -564
- package/src/jev-client.ts +12 -17
- package/src/jev-core.ts +137 -87
- 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/pi-surface.ts +34 -22
- package/src/pier-options.ts +54 -50
- package/src/pipe-channel.ts +27 -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 +728 -0
- package/src/{core → plugins}/terminal.ts +167 -127
- package/src/{core → plugins}/todo.ts +61 -100
- package/src/progress-core.ts +16 -53
- package/src/reconcile-core.ts +24 -33
- package/src/reducer-core.ts +24 -60
- package/src/reducer-invoker.ts +266 -239
- package/src/renderers.ts +76 -87
- package/src/role-loader.ts +33 -28
- package/src/role-manifest.ts +23 -28
- package/src/role-state.ts +119 -0
- 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 +52 -21
- 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 +253 -118
- package/src/subagent-gc.ts +109 -67
- package/src/subagent-poller.ts +367 -24
- package/src/subagent-session.ts +516 -0
- package/src/subagent-spawn.ts +530 -52
- package/src/swallow.ts +7 -17
- package/src/terminal-core.ts +125 -106
- package/src/todo-core.ts +57 -109
- package/src/todo-read-hook.ts +23 -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/vocab.ts +52 -25
- 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/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
package/src/subagent-gc.ts
CHANGED
|
@@ -1,30 +1,87 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Why: GC mixed herdr close I/O with the plugin body. Predicates stay in
|
|
5
|
-
* gc-core.ts; this adapter owns list/close, isolate git, and the ticker.
|
|
2
|
+
* Subagent board hygiene: registry projection + recovery, tab/pane collection, isolate-worktree sweep.
|
|
3
|
+
* Predicates live in gc-core.ts; this adapter owns herdr close I/O, isolate git commands, and the ticker.
|
|
6
4
|
*/
|
|
7
5
|
import { appendFileSync, existsSync, rmSync } from 'node:fs';
|
|
8
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
7
|
import type { HerdrClientLike } from './herdr-client.ts';
|
|
10
|
-
import { shouldClosePane, shouldCloseTaskTab } from './gc-core.ts';
|
|
8
|
+
import { isPathInside, planIsolateSweep, shouldClosePane, shouldCloseTaskTab } from './gc-core.ts';
|
|
11
9
|
import { runtimePolicy } from './runtime-policy.ts';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
10
|
+
import { swallow } from './swallow.ts';
|
|
11
|
+
import { evaluateRelease, foldSubsRegistry, makeRegistry, parseWorktreePorcelain, sleep, SUBS_CUSTOM_TYPE, type SubEntry } from './subagent-core.ts';
|
|
12
|
+
import type { GitIo } from './subagent-spawn.ts';
|
|
13
|
+
import type { TerminalStateSlot } from './plugins/terminal.ts';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
import type { GitIo } from './subagent-git-io.ts';
|
|
17
|
-
import type { TerminalStateSlot } from './core/terminal.ts';
|
|
15
|
+
/* ── registry projection + recovery ─────────────────────────────── */
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
interface SubagentRegistryHost {
|
|
18
|
+
pi: { appendEntry?: (customType: string, data: unknown) => void };
|
|
19
|
+
client: HerdrClientLike;
|
|
20
|
+
subs: Map<string, SubEntry>;
|
|
21
|
+
writeHistory: (entry: SubEntry, patch?: { status?: SubEntry['status']; closedAt?: number }, via?: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SubagentRegistry {
|
|
25
|
+
/** Append the registry snapshot to the session branch; duplicate snapshots are skipped. */
|
|
26
|
+
persist(): void;
|
|
27
|
+
rebuild(eventCtx: unknown): void;
|
|
28
|
+
sweepZombieRunning(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createSubagentRegistry(host: SubagentRegistryHost): SubagentRegistry {
|
|
32
|
+
let lastSnapshot = '';
|
|
33
|
+
|
|
34
|
+
function persist(): void {
|
|
35
|
+
try {
|
|
36
|
+
const registry = makeRegistry([...host.subs.values()]);
|
|
37
|
+
const snapshot = JSON.stringify(registry);
|
|
38
|
+
if (snapshot === lastSnapshot) return;
|
|
39
|
+
lastSnapshot = snapshot;
|
|
40
|
+
host.pi.appendEntry?.(SUBS_CUSTOM_TYPE, registry);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// Session logging must not break delegation; a silent failure here shows up as a ghost running row.
|
|
43
|
+
swallow('subagent.persist-subs', err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rebuild(eventCtx: unknown): void {
|
|
48
|
+
try {
|
|
49
|
+
const entries = (eventCtx as { sessionManager?: { getBranch?: () => readonly unknown[] } })
|
|
50
|
+
?.sessionManager?.getBranch?.() ?? [];
|
|
51
|
+
const registry = foldSubsRegistry(entries as Parameters<typeof foldSubsRegistry>[0]);
|
|
52
|
+
for (const entry of registry.subs) host.subs.set(entry.paneId, entry);
|
|
53
|
+
lastSnapshot = ''; // branch replay replaced live state; force the next persist
|
|
54
|
+
} catch {
|
|
55
|
+
/* registry recovery failure must not block the live session */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function sweepZombieRunning(): Promise<void> {
|
|
60
|
+
if (!host.client.available || host.subs.size === 0) return;
|
|
61
|
+
let livePaneIds: ReadonlySet<string>;
|
|
62
|
+
try {
|
|
63
|
+
livePaneIds = new Set((await host.client.listPanes()).map((pane) => pane.paneId));
|
|
64
|
+
} catch {
|
|
65
|
+
return; // do not close agents when the liveness lookup itself failed
|
|
66
|
+
}
|
|
67
|
+
let changed = false;
|
|
68
|
+
for (const [paneId, entry] of host.subs) {
|
|
69
|
+
if (entry.status !== 'running' || livePaneIds.has(paneId)) continue;
|
|
70
|
+
entry.status = 'closed';
|
|
71
|
+
host.writeHistory(entry, { status: 'closed', closedAt: Date.now() }, 'zombie-sweep');
|
|
72
|
+
changed = true;
|
|
73
|
+
}
|
|
74
|
+
if (changed) persist();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { persist, rebuild, sweepZombieRunning };
|
|
23
78
|
}
|
|
24
79
|
|
|
25
|
-
|
|
80
|
+
/* ── GC ─────────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
interface GcHost {
|
|
26
83
|
client: HerdrClientLike;
|
|
27
|
-
env: { tabId: string } | null;
|
|
84
|
+
env: { tabId: string; paneId?: string } | null;
|
|
28
85
|
subs: Map<string, SubEntry>;
|
|
29
86
|
persistSubs(): void;
|
|
30
87
|
writeHistory(e: SubEntry, patch?: { outcome?: string | null; status?: SubEntry['status']; closedAt?: number }, via?: string): void;
|
|
@@ -35,7 +92,7 @@ export interface GcHost {
|
|
|
35
92
|
injectNotice(content: string): Promise<void>;
|
|
36
93
|
}
|
|
37
94
|
|
|
38
|
-
|
|
95
|
+
interface GcController {
|
|
39
96
|
runGcSafely(): Promise<void>;
|
|
40
97
|
onTurnStart(): Promise<void>;
|
|
41
98
|
startTicker(ctx: Context): void;
|
|
@@ -66,66 +123,58 @@ export function createGcController(h: GcHost): GcController {
|
|
|
66
123
|
arr.push(e);
|
|
67
124
|
byTab.set(e.tabId, arr);
|
|
68
125
|
}
|
|
69
|
-
const taskTabIds = new Set(byTab.keys());
|
|
70
126
|
const mainTabId = h.env?.tabId ?? '';
|
|
71
127
|
|
|
128
|
+
const closeRow = (e: SubEntry): void => {
|
|
129
|
+
if (e.status === 'closed') return;
|
|
130
|
+
e.status = 'closed';
|
|
131
|
+
h.writeHistory(e, { status: 'closed', closedAt: Date.now() }, 'gc');
|
|
132
|
+
};
|
|
133
|
+
|
|
72
134
|
for (const [tabId, entries] of byTab) {
|
|
73
135
|
if (tabId === mainTabId) continue;
|
|
74
136
|
const tabPanes = panesList.filter((p) => p.tabId === tabId);
|
|
75
137
|
if (tabPanes.length === 0) {
|
|
76
|
-
for (const e of entries)
|
|
77
|
-
if (e.status !== 'closed') {
|
|
78
|
-
e.status = 'closed';
|
|
79
|
-
h.writeHistory(e, { status: 'closed', closedAt: Date.now() }, 'gc');
|
|
80
|
-
}
|
|
81
|
-
}
|
|
138
|
+
for (const e of entries) closeRow(e);
|
|
82
139
|
continue;
|
|
83
140
|
}
|
|
84
|
-
|
|
141
|
+
if (!autoCloseTabs) continue;
|
|
142
|
+
if (!shouldCloseTaskTab({
|
|
85
143
|
entries,
|
|
86
144
|
paneStatuses: tabPanes.map((p) => p.agentStatus),
|
|
87
145
|
ttlMs,
|
|
88
146
|
now: Date.now(),
|
|
89
|
-
});
|
|
90
|
-
|
|
147
|
+
})) continue;
|
|
148
|
+
// Live terminal panes and not-yet-delivered settlement notices keep the tab alive.
|
|
91
149
|
if (tabPanes.some((p) => termPaneIds.has(p.paneId) || pendingNoticeIds.has(p.paneId))) continue;
|
|
92
150
|
try {
|
|
93
151
|
await h.client.tabClose(tabId);
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
for (const e of entries) {
|
|
98
|
-
if (e.status !== 'closed') {
|
|
99
|
-
e.status = 'closed';
|
|
100
|
-
h.writeHistory(e, { status: 'closed', closedAt: Date.now() }, 'gc');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
152
|
+
} catch { /* tab may already be gone */ }
|
|
153
|
+
for (const e of entries) closeRow(e);
|
|
103
154
|
await sleep(300);
|
|
104
155
|
}
|
|
105
156
|
|
|
106
|
-
const closableTaskTabIds = new Set([...
|
|
157
|
+
const closableTaskTabIds = new Set([...byTab.keys()].filter((t) => t !== mainTabId));
|
|
107
158
|
const candidates = [...h.subs.values()].filter(
|
|
108
159
|
(e) => e.status === 'consumed' && !(e.tabId && closableTaskTabIds.has(e.tabId)),
|
|
109
160
|
);
|
|
110
161
|
for (const e of candidates) {
|
|
111
162
|
if (termPaneIds.has(e.paneId) || pendingNoticeIds.has(e.paneId)) continue;
|
|
163
|
+
// Never collect self, whatever the registry says: a poisoned entry once closed the master's pane.
|
|
164
|
+
if (h.env?.paneId && e.paneId === h.env.paneId) continue;
|
|
112
165
|
if (!shouldClosePane({
|
|
113
166
|
consumedAt: e.consumedAt ?? null,
|
|
114
167
|
herdrStatus: statuses.get(e.paneId),
|
|
115
168
|
prevTurnStart,
|
|
116
169
|
})) continue;
|
|
117
170
|
if (statuses.get(e.paneId) === undefined) {
|
|
118
|
-
e
|
|
119
|
-
h.writeHistory(e, { status: 'closed', closedAt: Date.now() }, 'gc');
|
|
171
|
+
closeRow(e);
|
|
120
172
|
continue;
|
|
121
173
|
}
|
|
122
174
|
try {
|
|
123
175
|
await h.client.closePane(e.paneId);
|
|
124
|
-
} catch {
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
e.status = 'closed';
|
|
128
|
-
h.writeHistory(e, { status: 'closed', closedAt: Date.now() }, 'gc');
|
|
176
|
+
} catch { /* pane may already be gone */ }
|
|
177
|
+
closeRow(e);
|
|
129
178
|
await sleep(300);
|
|
130
179
|
}
|
|
131
180
|
h.persistSubs();
|
|
@@ -143,12 +192,11 @@ export function createGcController(h: GcHost): GcController {
|
|
|
143
192
|
const wtPorcelain = await h.git.runGit(masterCwd, ['worktree', 'list', '--porcelain']);
|
|
144
193
|
if (wtPorcelain === null) return;
|
|
145
194
|
const wtByBranch = parseWorktreePorcelain(wtPorcelain);
|
|
146
|
-
// Session-owned isolates: settled or consumed entries whose worktree was not released yet.
|
|
147
195
|
const sessionOwned = [...h.subs.values()]
|
|
148
196
|
.filter((e) => e.isolate && e.isolate.releasedAt == null && e.status !== 'running')
|
|
149
197
|
.map((e) => ({ branch: e.isolate!.branch, worktreePath: e.isolate!.worktreePath }));
|
|
150
|
-
// Untracked `pier/*` branches are only swept on explicit request: they may belong to
|
|
151
|
-
//
|
|
198
|
+
// Untracked `pier/*` branches are only swept on explicit request: they may belong to another
|
|
199
|
+
// session whose process is even running inside them.
|
|
152
200
|
const sweepOrphans = process.env.PIER_ISOLATE_SWEEP_ORPHANS === '1';
|
|
153
201
|
const branches = sweepOrphans
|
|
154
202
|
? (await h.git.runGit(masterCwd, ['for-each-ref', '--format=%(refname:short)', 'refs/heads/pier/']))
|
|
@@ -167,14 +215,12 @@ export function createGcController(h: GcHost): GcController {
|
|
|
167
215
|
for (const e of h.subs.values()) {
|
|
168
216
|
if (e.isolate && e.isolate.releasedAt == null && e.status !== 'running') entryByBranch.set(e.isolate.branch, e);
|
|
169
217
|
}
|
|
170
|
-
|
|
171
|
-
for (const cand of plan.candidates) {
|
|
172
|
-
byBranch.set(cand.branch, { branch: cand.branch, wtPath: cand.worktreePath, entry: entryByBranch.get(cand.branch) ?? null });
|
|
173
|
-
}
|
|
174
|
-
trace?.(`cands=${[...byBranch.keys()].join(',') || 'none'} skipped=${plan.skipped.map((s) => `${s.branch}:${s.reason}`).join(',') || 'none'} subs=${[...h.subs.values()].map((s) => `${s.status}${s.isolate ? '/iso' : ''}`).join(',') || 'none'}`);
|
|
218
|
+
trace?.(`cands=${plan.candidates.map((c) => c.branch).join(',') || 'none'} skipped=${plan.skipped.map((s) => `${s.branch}:${s.reason}`).join(',') || 'none'} subs=${[...h.subs.values()].map((s) => `${s.status}${s.isolate ? '/iso' : ''}`).join(',') || 'none'}`);
|
|
175
219
|
let persisted = false;
|
|
176
|
-
for (const cand of
|
|
177
|
-
const { branch, wtPath
|
|
220
|
+
for (const cand of plan.candidates) {
|
|
221
|
+
const { branch, worktreePath: wtPath } = cand;
|
|
222
|
+
const entry = entryByBranch.get(branch) ?? null;
|
|
223
|
+
// The worktree is already gone (removed by hand or by a previous pass) → only release the row.
|
|
178
224
|
if (entry && !wtByBranch.has(branch)) {
|
|
179
225
|
if (existsSync(wtPath)) {
|
|
180
226
|
try { rmSync(wtPath, { recursive: true, force: true }); } catch { continue; }
|
|
@@ -185,9 +231,10 @@ export function createGcController(h: GcHost): GcController {
|
|
|
185
231
|
continue;
|
|
186
232
|
}
|
|
187
233
|
const mergedOut = await h.git.runGit(masterCwd, ['merge-base', '--is-ancestor', branch, 'HEAD']);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
234
|
+
let mergedFinal: boolean | null = mergedOut !== null ? true : null;
|
|
235
|
+
if (mergedFinal === null) {
|
|
236
|
+
// A non-zero exit means "not an ancestor" OR "unknown branch": only a resolving ref
|
|
237
|
+
// makes it unmerged; otherwise the decision stays 'unknown' and the worktree is retained.
|
|
191
238
|
const sha = await h.git.runGit(masterCwd, ['rev-parse', branch]);
|
|
192
239
|
const headSha = await h.git.runGit(masterCwd, ['rev-parse', 'HEAD']);
|
|
193
240
|
if (sha != null && headSha != null) mergedFinal = false;
|
|
@@ -196,11 +243,10 @@ export function createGcController(h: GcHost): GcController {
|
|
|
196
243
|
const dirtyCount = dirtyOut === null ? null : dirtyOut.split('\n').filter((l) => l.trim() !== '').length;
|
|
197
244
|
const decision = evaluateRelease({ merged: mergedFinal, dirtyCount });
|
|
198
245
|
if (decision.action === 'release') {
|
|
199
|
-
// Defense in depth: planner already refuses the current worktree, but a removal here
|
|
200
|
-
// must never
|
|
246
|
+
// Defense in depth: the planner already refuses the current worktree, but a removal here
|
|
247
|
+
// must never delete the directory this process is running in.
|
|
201
248
|
if (isPathInside(masterCwd, wtPath)) continue;
|
|
202
|
-
|
|
203
|
-
let ok = removed !== null;
|
|
249
|
+
let ok = (await h.git.runGit(masterCwd, ['worktree', 'remove', wtPath])) !== null;
|
|
204
250
|
if (!ok) {
|
|
205
251
|
await sleep(2000);
|
|
206
252
|
ok = (await h.git.runGit(masterCwd, ['worktree', 'remove', wtPath])) !== null;
|
|
@@ -227,14 +273,10 @@ export function createGcController(h: GcHost): GcController {
|
|
|
227
273
|
gcRunning = true;
|
|
228
274
|
try {
|
|
229
275
|
await gcPass();
|
|
230
|
-
} catch {
|
|
231
|
-
/* next turn/tick retries */
|
|
232
|
-
}
|
|
276
|
+
} catch { /* next turn/tick retries */ }
|
|
233
277
|
try {
|
|
234
278
|
await isolateSweep();
|
|
235
|
-
} catch {
|
|
236
|
-
/* next turn/tick retries */
|
|
237
|
-
} finally {
|
|
279
|
+
} catch { /* next turn/tick retries */ } finally {
|
|
238
280
|
gcRunning = false;
|
|
239
281
|
}
|
|
240
282
|
}
|