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
package/src/efficiency-store.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* D101-D103
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Evidence-Preserving Reducer, along with telemetry log appending and
|
|
6
|
-
* secure bash un-truncated output reading.
|
|
7
|
-
*
|
|
8
|
-
* Security & Integrity:
|
|
9
|
-
* - Mode 0700 for directories, 0600 for object and log files.
|
|
10
|
-
* - O_NOFOLLOW to forbid symlinks.
|
|
11
|
-
* - Safe sessionId regex validation.
|
|
12
|
-
* - EEXIST integrity verification (rejects corrupted collisions).
|
|
2
|
+
* D101-D103 efficiency storage: content-addressed objects (ObservationPack / EPR), telemetry appends
|
|
3
|
+
* and the un-truncated bash log reader. Integrity: directories 0700, files 0600, O_NOFOLLOW everywhere,
|
|
4
|
+
* session ids regex-validated, and an EEXIST collision is re-verified (size + sha256) instead of trusted.
|
|
13
5
|
*/
|
|
14
6
|
|
|
15
7
|
import { constants } from 'node:fs';
|
|
@@ -23,8 +15,8 @@ import {
|
|
|
23
15
|
type RecallSliceResult,
|
|
24
16
|
} from './observation-core.ts';
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
19
|
+
const BASH_LOG_NAME_RE = /^pi-bash-.*\.log$/;
|
|
28
20
|
|
|
29
21
|
const READ_OBJECT_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW;
|
|
30
22
|
const CREATE_OBJECT_FLAGS =
|
|
@@ -66,15 +58,15 @@ export function reducerObjectPath(root: string, sha256: string): string {
|
|
|
66
58
|
|
|
67
59
|
export function efficiencyLogPath(
|
|
68
60
|
root: string,
|
|
69
|
-
mechanism: 'compact' | 'observation' | 'reducer' | 'jev',
|
|
61
|
+
mechanism: 'compact' | 'observation' | 'reducer' | 'jev' | 'routing' | 'tools',
|
|
70
62
|
customPath?: string,
|
|
71
63
|
): string {
|
|
72
64
|
if (customPath && typeof customPath === 'string') return resolve(customPath);
|
|
73
65
|
return join(root, 'efficiency-logs', `${mechanism}.jsonl`);
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
const MAX_OBJECTS_PER_DIR = 300;
|
|
69
|
+
const MAX_OBJECTS_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
78
70
|
|
|
79
71
|
export async function pruneObjectsDirectory(
|
|
80
72
|
dir: string,
|
|
@@ -98,7 +90,6 @@ export async function pruneObjectsDirectory(
|
|
|
98
90
|
totalBytes += s.size;
|
|
99
91
|
}
|
|
100
92
|
} catch {
|
|
101
|
-
/* ignore stat errors */
|
|
102
93
|
}
|
|
103
94
|
}
|
|
104
95
|
|
|
@@ -106,7 +97,6 @@ export async function pruneObjectsDirectory(
|
|
|
106
97
|
return 0;
|
|
107
98
|
}
|
|
108
99
|
|
|
109
|
-
// Sort by mtime ascending (oldest first)
|
|
110
100
|
files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
111
101
|
|
|
112
102
|
let removedCount = 0;
|
|
@@ -122,7 +112,6 @@ export async function pruneObjectsDirectory(
|
|
|
122
112
|
if (k.startsWith(f.path)) verifiedObjectCache.delete(k);
|
|
123
113
|
}
|
|
124
114
|
} catch {
|
|
125
|
-
/* ignore unlink errors */
|
|
126
115
|
}
|
|
127
116
|
}
|
|
128
117
|
return removedCount;
|
|
@@ -131,7 +120,6 @@ export async function pruneObjectsDirectory(
|
|
|
131
120
|
}
|
|
132
121
|
}
|
|
133
122
|
|
|
134
|
-
/** Prune both content-addressed object dirs of one session root; returns total removed files. */
|
|
135
123
|
export async function pruneSessionObjects(
|
|
136
124
|
sessionRoot: string,
|
|
137
125
|
limits: { maxFiles?: number; maxTotalBytes?: number } = {},
|
|
@@ -173,7 +161,6 @@ export async function storeContentAddressedObject(
|
|
|
173
161
|
verifiedObjectCache.add(`${filePath}:${contentBytes}:${writtenStat.mtimeMs}:${hash}`);
|
|
174
162
|
} catch (err) {
|
|
175
163
|
if (err instanceof Error && 'code' in err && err.code === 'EEXIST') {
|
|
176
|
-
// Verify existing file integrity
|
|
177
164
|
const existingHandle = await open(filePath, READ_OBJECT_FLAGS);
|
|
178
165
|
try {
|
|
179
166
|
const existingStat = await existingHandle.stat();
|
|
@@ -234,13 +221,18 @@ export async function readStoredObjectChunk(
|
|
|
234
221
|
const available = Math.max(0, fileStat.size - offset);
|
|
235
222
|
const buffer = Buffer.alloc(Math.min(available, limits.maxBytes + 4));
|
|
236
223
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
|
|
237
|
-
|
|
224
|
+
const sliced = sliceBufferChunk(buffer.subarray(0, bytesRead), 0, limits);
|
|
225
|
+
// sliceBufferChunk's nextOffset is relative to the window it was given (offset 0). Callers page
|
|
226
|
+
// with the absolute file offset from the header, so add the seek back.
|
|
227
|
+
const nextOffset = offset + sliced.nextOffset;
|
|
228
|
+
return { ...sliced, nextOffset, eof: nextOffset >= fileStat.size };
|
|
238
229
|
} finally {
|
|
239
230
|
await handle?.close();
|
|
240
231
|
}
|
|
241
232
|
}
|
|
242
233
|
|
|
243
|
-
|
|
234
|
+
/** Rotation ceiling: rename to `<log>.old` past this size so a log can never grow unbounded. */
|
|
235
|
+
const MAX_EFFICIENCY_LOG_BYTES = 5 * 1024 * 1024;
|
|
244
236
|
|
|
245
237
|
export async function appendEfficiencyLog(
|
|
246
238
|
logPath: string,
|
|
@@ -250,7 +242,6 @@ export async function appendEfficiencyLog(
|
|
|
250
242
|
const dir = dirname(logPath);
|
|
251
243
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
252
244
|
|
|
253
|
-
// Rotate log if exceeds maximum size to prevent unbounded disk growth
|
|
254
245
|
try {
|
|
255
246
|
const s = await stat(logPath);
|
|
256
247
|
if (s.size > MAX_EFFICIENCY_LOG_BYTES) {
|
|
@@ -259,7 +250,6 @@ export async function appendEfficiencyLog(
|
|
|
259
250
|
await rename(logPath, oldPath);
|
|
260
251
|
}
|
|
261
252
|
} catch {
|
|
262
|
-
/* file may not exist yet, continue */
|
|
263
253
|
}
|
|
264
254
|
|
|
265
255
|
let handle;
|
|
@@ -271,9 +261,7 @@ export async function appendEfficiencyLog(
|
|
|
271
261
|
}
|
|
272
262
|
}
|
|
273
263
|
|
|
274
|
-
/**
|
|
275
|
-
* Safely reads Pi's untruncated bash output from temporary file.
|
|
276
|
-
*/
|
|
264
|
+
/** Safely reads Pi's untruncated bash output from temporary file. */
|
|
277
265
|
export async function readBashFullOutput(
|
|
278
266
|
rawPath: string | null | undefined,
|
|
279
267
|
maxChars: number,
|
|
@@ -292,7 +280,6 @@ export async function readBashFullOutput(
|
|
|
292
280
|
const fileStat = await stat(realFile);
|
|
293
281
|
if (!fileStat.isFile() || fileStat.isSymbolicLink()) return null;
|
|
294
282
|
|
|
295
|
-
// Check size within reasonable limit
|
|
296
283
|
if (fileStat.size > maxChars * 4) return null;
|
|
297
284
|
|
|
298
285
|
let handle = await open(realFile, READ_OBJECT_FLAGS);
|
package/src/gc-core.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task-tab GC decisions (D29 rules as a pure function; M22 dropped resident exemption).
|
|
3
|
-
* No I/O, no pi/herdr — unit-testable.
|
|
4
|
-
*/
|
|
1
|
+
/** Task-tab GC decisions (D29 rules as a pure function; no I/O, no pi/herdr). */
|
|
5
2
|
export type GcEntryKind = string;
|
|
6
3
|
export type GcEntryStatus = 'running' | 'settled' | 'consumed' | 'closed';
|
|
7
4
|
|
|
@@ -17,14 +14,8 @@ function isFinished(status: GcEntryStatus): boolean {
|
|
|
17
14
|
return status === 'consumed' || status === 'closed' || status === 'settled';
|
|
18
15
|
}
|
|
19
16
|
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
* - ≥1 work pane (the main tab with no delegated entries never closes);
|
|
23
|
-
* - every work pane is settled/consumed/closed (kind is not an exemption);
|
|
24
|
-
* - grace TTL elapsed (`ttlMs=0` means never auto-close);
|
|
25
|
-
* - no blocked pane (human-gate exemption);
|
|
26
|
-
* - remaining panes are idle/done/unknown (non-work panes may be unknown).
|
|
27
|
-
*/
|
|
17
|
+
/** Close a task tab only when every work pane finished, the grace TTL elapsed (`ttlMs=0` means never
|
|
18
|
+
* auto-close), no pane is blocked and the rest are idle/done/unknown. */
|
|
28
19
|
export function shouldCloseTaskTab(opts: {
|
|
29
20
|
entries: readonly GcEntryLike[];
|
|
30
21
|
paneStatuses: readonly string[];
|
|
@@ -39,12 +30,8 @@ export function shouldCloseTaskTab(opts: {
|
|
|
39
30
|
return true;
|
|
40
31
|
}
|
|
41
32
|
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
* - consumed before the previous turn (grace so the settlement notice is still visible);
|
|
45
|
-
* - herdr status idle/done (unknown/working/blocked retry next turn);
|
|
46
|
-
* - missing pane (status undefined) → record closed (caller handles the write).
|
|
47
|
-
*/
|
|
33
|
+
/** Pane-level collection (orphan / compat path): consumed before the previous turn (grace so the
|
|
34
|
+
* settlement notice is still visible); missing pane → record closed (caller handles the write). */
|
|
48
35
|
export function shouldClosePane(opts: {
|
|
49
36
|
consumedAt: number | null;
|
|
50
37
|
herdrStatus: string | undefined;
|
|
@@ -55,18 +42,11 @@ export function shouldClosePane(opts: {
|
|
|
55
42
|
return (opts.consumedAt ?? 0) > 0 && opts.consumedAt! < opts.prevTurnStart;
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
/*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* HEAD and clean once it commits — so the sweep deleted the worktree the process
|
|
63
|
-
* was running in (observed twice: 01a06ae3-era workers and the env-hook worker).
|
|
64
|
-
* Ownership must therefore be explicit: only branches registered in THIS session's
|
|
65
|
-
* subagent registry are candidates, the current working directory is never a
|
|
66
|
-
* candidate, and untracked `pier/*` branches require an explicit opt-in.
|
|
67
|
-
*/
|
|
45
|
+
/* Isolate worktree collection is deliberately narrow: `refs/heads/pier/*` is NOT proof of pier
|
|
46
|
+
* ownership — a worker session lives in such a worktree, so a namespace-wide sweep deletes the
|
|
47
|
+
* worktree its own process is running in. Only THIS session's registered branches are candidates,
|
|
48
|
+
* cwd never is, and untracked `pier/*` branches need an explicit opt-in. */
|
|
68
49
|
|
|
69
|
-
/** True when `path` is `parent` or lives inside it (both resolved, no I/O). */
|
|
70
50
|
export function isPathInside(path: string, parent: string): boolean {
|
|
71
51
|
const norm = (p: string): string => p.replace(/[\\/]+/g, '/').replace(/\/+$/, '');
|
|
72
52
|
const child = norm(path);
|
|
@@ -90,20 +70,14 @@ export interface IsolateSweepPlan {
|
|
|
90
70
|
skipped: IsolateSweepSkip[];
|
|
91
71
|
}
|
|
92
72
|
|
|
93
|
-
/**
|
|
94
|
-
* Decide which isolate worktrees this session may collect.
|
|
95
|
-
* Pure: callers pass git output, the subagent registry and the process cwd.
|
|
96
|
-
*/
|
|
73
|
+
/** Pure: callers pass git output, the subagent registry and the process cwd. */
|
|
97
74
|
export function planIsolateSweep(input: {
|
|
98
|
-
/** Every `refs/heads/pier/*` branch name. */
|
|
99
75
|
branches: readonly string[];
|
|
100
76
|
/** branch → worktree path, from `git worktree list --porcelain`. */
|
|
101
77
|
worktreesByBranch: ReadonlyMap<string, string>;
|
|
102
|
-
/** Branches of isolate entries in this session's registry. */
|
|
103
78
|
registeredBranches: ReadonlySet<string>;
|
|
104
79
|
/** Branches created but not yet registered (worktree add → subs.set window). */
|
|
105
80
|
pendingBranches: ReadonlySet<string>;
|
|
106
|
-
/** Session-owned isolates that may be collected, with their recorded path. */
|
|
107
81
|
sessionOwned: ReadonlyArray<{ branch: string; worktreePath: string }>;
|
|
108
82
|
/** Path to protect (normally process.cwd()). */
|
|
109
83
|
cwd: string;
|
package/src/guards.ts
ADDED