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.
Files changed (103) hide show
  1. package/README.md +16 -12
  2. package/package.json +3 -3
  3. package/schemas/role-manifest.schema.json +7 -1
  4. package/skills/pi-herdr-workflow/SKILL.md +4 -5
  5. package/src/ansi-text.ts +62 -11
  6. package/src/ask-dialog.ts +308 -0
  7. package/src/ask-user.ts +113 -112
  8. package/src/bootstrap.ts +10 -13
  9. package/src/compact-coordinator.ts +279 -261
  10. package/src/compact-economics-core.ts +77 -145
  11. package/src/config-catalog-core.ts +77 -197
  12. package/src/config-command.ts +271 -64
  13. package/src/cordis-services.ts +64 -0
  14. package/src/dashboard-command.ts +77 -33
  15. package/src/delegation-core.ts +115 -0
  16. package/src/efficiency-config-core.ts +286 -486
  17. package/src/efficiency-store.ts +16 -29
  18. package/src/gc-core.ts +10 -36
  19. package/src/guards.ts +6 -0
  20. package/src/herdr-client.ts +202 -233
  21. package/src/history-store.ts +38 -23
  22. package/src/index-gates.ts +118 -0
  23. package/src/index-locks.ts +2 -7
  24. package/src/index-master.ts +31 -29
  25. package/src/index-notices.ts +28 -15
  26. package/src/index-pipe.ts +31 -12
  27. package/src/index-roles.ts +306 -0
  28. package/src/index-runtime.ts +16 -6
  29. package/src/index-worker.ts +8 -18
  30. package/src/index.ts +561 -570
  31. package/src/jev-client.ts +12 -17
  32. package/src/jev-core.ts +138 -90
  33. package/src/ledger.ts +13 -21
  34. package/src/lock-core.ts +28 -56
  35. package/src/manifest-compose.ts +35 -20
  36. package/src/observation-core.ts +52 -100
  37. package/src/pane-title.ts +19 -38
  38. package/src/panel-open.ts +47 -0
  39. package/src/pi-surface.ts +34 -22
  40. package/src/pier-options.ts +53 -50
  41. package/src/pipe-channel.ts +44 -59
  42. package/src/platform-paths.ts +19 -52
  43. package/src/{core → plugins}/grid-shape.ts +8 -10
  44. package/src/{core → plugins}/heat-plan.ts +20 -22
  45. package/src/plugins/observation.ts +475 -0
  46. package/src/plugins/subagent.ts +766 -0
  47. package/src/{core → plugins}/terminal.ts +198 -139
  48. package/src/{core → plugins}/todo.ts +73 -134
  49. package/src/progress-core.ts +16 -53
  50. package/src/reconcile-core.ts +24 -33
  51. package/src/reducer-core.ts +26 -64
  52. package/src/reducer-invoker.ts +266 -239
  53. package/src/renderers.ts +108 -84
  54. package/src/role-loader.ts +33 -28
  55. package/src/role-manifest.ts +25 -31
  56. package/src/role-state.ts +119 -0
  57. package/src/roles/worker-default.json +3 -3
  58. package/src/routing-telemetry.ts +136 -0
  59. package/src/runtime-policy.ts +25 -40
  60. package/src/session-tail.ts +65 -41
  61. package/src/settle-wake-core.ts +61 -22
  62. package/src/slim-frame.ts +25 -122
  63. package/src/stale-core.ts +17 -24
  64. package/src/storage-layout.ts +14 -43
  65. package/src/subagent-core.ts +423 -120
  66. package/src/subagent-gc.ts +109 -67
  67. package/src/subagent-poller.ts +383 -24
  68. package/src/subagent-session.ts +516 -0
  69. package/src/subagent-spawn.ts +577 -52
  70. package/src/swallow.ts +7 -17
  71. package/src/terminal-core.ts +187 -134
  72. package/src/todo-core.ts +57 -109
  73. package/src/todo-read-hook.ts +46 -39
  74. package/src/todo-reminder-core.ts +26 -51
  75. package/src/todo-window.ts +11 -12
  76. package/src/todos-service.ts +15 -35
  77. package/src/tool-error.ts +6 -13
  78. package/src/tool-gate.ts +38 -21
  79. package/src/tree-view.ts +162 -0
  80. package/src/vocab.ts +52 -25
  81. package/src/worker-marker.ts +76 -0
  82. package/src/ask-multi.ts +0 -260
  83. package/src/config-guide.ts +0 -361
  84. package/src/core/observation.ts +0 -555
  85. package/src/core/subagent.ts +0 -633
  86. package/src/efficiency-config-io.ts +0 -53
  87. package/src/focus-poller.ts +0 -244
  88. package/src/git-adapter.ts +0 -113
  89. package/src/notice-buffer.ts +0 -23
  90. package/src/subagent-execute.ts +0 -328
  91. package/src/subagent-git-io.ts +0 -80
  92. package/src/subagent-launch.ts +0 -114
  93. package/src/subagent-list-action.ts +0 -69
  94. package/src/subagent-output-action.ts +0 -104
  95. package/src/subagent-output-adapter.ts +0 -27
  96. package/src/subagent-output-core.ts +0 -263
  97. package/src/subagent-poll-loop.ts +0 -305
  98. package/src/subagent-port.ts +0 -27
  99. package/src/subagent-ready.ts +0 -89
  100. package/src/subagent-registry.ts +0 -70
  101. package/src/subagent-resolution.ts +0 -41
  102. package/src/subagent-scope.ts +0 -41
  103. package/src/subagent-session-io.ts +0 -230
@@ -1,15 +1,7 @@
1
1
  /**
2
- * D101-D103 Efficiency Storage & I/O Adapter.
3
- *
4
- * Implements safe, content-addressed storage for ObservationPack and
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
- export const SAFE_SESSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/i;
27
- export const BASH_LOG_NAME_RE = /^pi-bash-.*\.log$/;
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
- export const MAX_OBJECTS_PER_DIR = 300;
77
- export const MAX_OBJECTS_TOTAL_BYTES = 50 * 1024 * 1024; // 50MB
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
- return sliceBufferChunk(buffer.subarray(0, bytesRead), 0, limits);
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
- export const MAX_EFFICIENCY_LOG_BYTES = 5 * 1024 * 1024; // 5MB rotation ceiling
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
- * Close a task tab only when every condition holds:
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
- * Pane-level collection (orphan / compat path):
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
- /* ── Isolate worktree collection (why the rules are this narrow) ──────
59
- * The first version treated ANY branch under `refs/heads/pier/` as pier-owned, so
60
- * every pier master session swept the whole namespace. A worker session lives in
61
- * exactly such a worktree, and its own branch is trivially an ancestor of its own
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
@@ -0,0 +1,6 @@
1
+ /** Shared structural guards used by the *-core validators. */
2
+
3
+ /** Plain JSON object: not null, not an array. */
4
+ export function isPlainObject(v: unknown): v is Record<string, unknown> {
5
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
6
+ }