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,31 +1,22 @@
1
- /**
2
- * D102 Evidence-Preserving Reducer Core.
3
- *
4
- * Pure algorithmic functions for diagnostic command recognition, secret filtering,
5
- * receipt schema checking, and byte-for-byte quote verification.
6
- *
7
- * No I/O in this core; storage I/O lives in efficiency-store.ts.
8
- */
1
+ /** D102 Evidence-Preserving Reducer core: diagnostic-command recognition, secret filtering, receipt
2
+ * schema checking, byte-for-byte quote verification (storage I/O lives in efficiency-store.ts). */
3
+ import { isPlainObject } from './guards.ts';
9
4
 
10
5
  import { createHash } from 'node:crypto';
11
6
 
12
7
  export const REDUCER_RECEIPT_SCHEMA = 'sol-pi-evidence-receipt/1' as const;
13
8
  export const REDUCER_RECEIPT_PREFIX = 'sol_pi_evidence_receipt_v1' as const;
14
9
 
15
- export const MAX_EVIDENCE_ITEMS = 12;
16
- export const MAX_QUOTE_CHARS = 600;
10
+ const MAX_EVIDENCE_ITEMS = 12;
11
+ const MAX_QUOTE_CHARS = 600;
17
12
 
18
13
  export const DEFAULT_MIN_BYTES = 4096;
19
14
  export const DEFAULT_MAX_CHARS = 600_000;
20
15
  export const DEFAULT_MAX_OUTPUT_TOKENS = 2048;
21
16
  export const DEFAULT_TIMEOUT_MS = 5000;
22
17
 
23
- /**
24
- * Diagnostic (test/build) command gate for EPR.
25
- * Boundaries accept any shell separator on both sides so that subshells and chains such as
26
- * `(npm test)`, `npm test&&echo ok` or `pytest;` are still recognized; a word character
27
- * after the keyword (`makefile`, `coqtop`, `npm run test`) must NOT match.
28
- */
18
+ /** EPR diagnostic (test/build) gate: any shell separator matches on either side, so subshells and
19
+ * chains like `(npm test)`/`npm test&&echo ok`/`pytest;` still match, while `makefile`/`coqtop` must not. */
29
20
  export const DIAGNOSTIC_COMMAND =
30
21
  /(?:^|[;&|()\s])(?:lake\s+build|lake\s+env\s+lean|lean|coq|cargo(?:\s+(?:build|test|check))?|zig\s+build|pytest|python(?:3)?\s+-m\s+(?:pytest|unittest|py_compile)|ctest|cmake\s+--build|ninja|make|npm\s+test|pnpm\s+test|yarn\s+test|go\s+test|bazel\s+test|node\s+--test|npx\s+tsx\s+--test|vitest|jest|mvn|mvnw|gradle|gradlew)(?:[;&|()\s]|$)/i;
31
22
 
@@ -33,14 +24,9 @@ export const FAILURE_SIGNAL =
33
24
  /error|failed|failure|fatal|exception|panic|timeout|unsolved|type mismatch|assert/i;
34
25
 
35
26
  /**
36
- * Credential heuristics for the EPR fail-open gate.
37
- *
38
- * Precision comes from the VALUE shape, not the keyword: after the separator the
39
- * text must contain a token-looking run — a known credential prefix (sk-, ghp_,
40
- * github_pat_, xox*, AKIA, eyJ) or >= 15 contiguous token characters containing
41
- * a digit. Keywords alone matched test names (`containsLikelySecret: detects …`),
42
- * JSON keys (`"Authorization":[]`), URL paths and function calls, which blocked
43
- * pier's own `npm test` output 7/7 times during the 2026-09-17 trial.
27
+ * Credential heuristics for the EPR fail-open gate: precision comes from the VALUE shape, not the
28
+ * keyword — after the separator the text must carry a token-looking run (a known credential prefix
29
+ * such as sk-, ghp_, github_pat_, xox*, AKIA or eyJ, or >= 15 contiguous token characters with a digit).
44
30
  */
45
31
  export const LIKELY_SECRET =
46
32
  /(?:api[_-]?key|api[_-]?secret|access[_-]?token|refresh[_-]?token|secret[_-]?key|client[_-]?secret|authorization|bearer|password|passwd|secret)[^\n]{0,40}[=:][\s"']*(?:bearer\s+|basic\s+)?[\s"']*(?:(?:sk-|ghp_|github_pat_|xox[baprs]-|AKIA|eyJ)[A-Za-z0-9_-]{10,}|(?=[A-Za-z0-9_\-.+~]*[0-9])[A-Za-z0-9_\-.+~]{15,})/i;
@@ -73,19 +59,11 @@ export function isDiagnosticCommand(command: string): boolean {
73
59
  }
74
60
 
75
61
  /**
76
- * Recover the untruncated-log path from Pi's inline truncation notice.
77
- *
78
- * Pi appends one of these to a large bash tool result:
79
- * `[Output truncated. Full output: /tmp/pi-bash-abc.log]`
80
- * `[Showing lines 1-2000 of 5000. Full output: /tmp/pi-bash-abc.log]`
81
- * `[Showing last 50KB of line 12 (line is 80KB). Full output: /tmp/pi-bash-abc.log]`
62
+ * Recover the untruncated-log path from Pi's inline notice (`[Output truncated. Full output: …]`,
63
+ * `[Showing lines 1-2000 of 5000. Full output: …]`): a replayed or re-shaped event can keep only that
64
+ * text, and verifying a receipt against a truncated preview would be wrong.
82
65
  *
83
- * `details.fullOutputPath` carries the same value, but a replayed or re-shaped event can keep
84
- * only the text. Recovering the path there lets the receipt be verified against the exact bytes
85
- * the command produced instead of a truncated preview.
86
- *
87
- * The returned path is NOT trusted: callers must still gate it (see `readBashFullOutput`, which
88
- * requires a `pi-bash-*.log` basename resolving to a regular non-symlink file inside tmpdir).
66
+ * The returned path is NOT trusted: callers must still gate it (see `readBashFullOutput`).
89
67
  */
90
68
  export function fullOutputPathFromNotice(text: string): string | undefined {
91
69
  // Only a bracketed notice counts, and Pi appends it at the very end — so take the LAST match.
@@ -103,10 +81,9 @@ export function containsLikelySecret(text: string): boolean {
103
81
  }
104
82
 
105
83
  /**
106
- * First matched credential-shaped snippet for reducer.jsonl fallback rows, with
107
- * the VALUE masked to its length — the keyword and separator are not secret and
108
- * identify the false-positive shape (test name vs JSON key vs real credential);
109
- * the value never enters telemetry (contract pinned by reducer-integration test).
84
+ * First matched credential-shaped snippet for reducer.jsonl fallback rows, with the VALUE masked to
85
+ * its length: the keyword and separator identify the false-positive shape (test name vs JSON key vs
86
+ * real credential) while the value never enters telemetry.
110
87
  */
111
88
  export function extractLikelySecretMatch(text: string): string | undefined {
112
89
  const match = LIKELY_SECRET.exec(text);
@@ -120,22 +97,16 @@ export function extractLikelySecretMatch(text: string): string | undefined {
120
97
  return `${snippet.slice(0, sep + 1)} <redacted:${value.length} chars,sha8=${sha256Hex(value).slice(0, 8)}>`;
121
98
  }
122
99
 
123
- /**
124
- * Reducer models frequently wrap their JSON in a Markdown fence even when told
125
- * not to; strip it before parsing (2026-09-17 trial: 3/8 workbench attempts
126
- * failed as invalid-json, same command as successes).
127
- */
128
- export function stripReceiptJsonFences(raw: string): string {
100
+ /** Reducer models frequently wrap their JSON in a Markdown fence even when told not to; strip it
101
+ * before parsing, or the receipt is rejected as invalid-json. */
102
+ function stripReceiptJsonFences(raw: string): string {
129
103
  const trimmed = raw.trim();
130
104
  const fenced = /^```[a-zA-Z0-9_-]*\s*\n([\s\S]*?)\n?```$/.exec(trimmed);
131
105
  return fenced?.[1]?.trim() ?? trimmed;
132
106
  }
133
107
 
134
- export function isRecord(v: unknown): v is Record<string, unknown> {
135
- return typeof v === 'object' && v !== null && !Array.isArray(v);
136
- }
137
108
 
138
- export function lineNumberOf(body: string, quote: string): number | undefined {
109
+ function lineNumberOf(body: string, quote: string): number | undefined {
139
110
  const index = body.indexOf(quote);
140
111
  if (index < 0) return undefined;
141
112
  let line = 1;
@@ -182,9 +153,6 @@ export function reducerInputPrompt(opts: {
182
153
  ].join('\n');
183
154
  }
184
155
 
185
- /**
186
- * Validates receipt byte for byte against the actual source text.
187
- */
188
156
  export function validateReceipt(
189
157
  rawJson: string,
190
158
  sourceHash: string,
@@ -198,7 +166,7 @@ export function validateReceipt(
198
166
  return { ok: false, reason: 'invalid-json' };
199
167
  }
200
168
 
201
- if (!isRecord(parsed)) {
169
+ if (!isPlainObject(parsed)) {
202
170
  return { ok: false, reason: 'schema-mismatch' };
203
171
  }
204
172
 
@@ -219,7 +187,7 @@ export function validateReceipt(
219
187
  const seen = new Set<string>();
220
188
 
221
189
  for (const item of parsed.evidence) {
222
- if (!isRecord(item)) {
190
+ if (!isPlainObject(item)) {
223
191
  return { ok: false, reason: 'schema-mismatch' };
224
192
  }
225
193
  const kind = item.kind;
@@ -310,14 +278,9 @@ export function formatReceiptText(opts: {
310
278
  }
311
279
 
312
280
  /**
313
- * Usage shapes accepted from / returned to pi.
314
- *
315
- * pi persists a tool result's `usage` into the session and renders it in the footer through
316
- * `addUsageToTotals`, which reads `usage.cost.total` WITHOUT a guard. Returning a partial object
317
- * (for example only `{ input, output, totalTokens }`) therefore crashes the whole pi process with
318
- * "TypeError: Cannot read properties of undefined (reading 'total')" — observed 2026-09-13 in a
319
- * subagent pane (stack: FooterComponent.render -> addUsageToTotals). pi's own definition lives in
320
- * docs/session-format.md; always emit the complete `UsageTotals` below.
281
+ * Usage shapes accepted from / returned to pi: `addUsageToTotals` reads `usage.cost.total` WITHOUT a
282
+ * guard, so a partial object crashes the whole pi process. Always emit the complete `UsageTotals`
283
+ * below; see docs/session-format.md.
321
284
  */
322
285
  export interface UsageLike {
323
286
  input?: number;
@@ -338,7 +301,6 @@ export interface UsageTotals {
338
301
  cost: { input: number; output: number; cacheRead: number; cacheWrite: number; total: number };
339
302
  }
340
303
 
341
- /** Sum two (possibly partial) usage records into one complete `UsageTotals`. */
342
304
  export function mergeUsage(a?: UsageLike, b?: UsageLike): UsageTotals {
343
305
  const sum = (x?: number, y?: number): number => (x ?? 0) + (y ?? 0);
344
306
  return {