pi-pier 0.2.7 → 0.3.0

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 (72) hide show
  1. package/README.md +20 -7
  2. package/package.json +7 -2
  3. package/schemas/efficiency-config.schema.json +77 -0
  4. package/skills/pi-herdr-workflow/SKILL.md +12 -0
  5. package/src/ansi-text.ts +83 -0
  6. package/src/ask-multi.ts +260 -0
  7. package/src/ask-user.ts +60 -10
  8. package/src/bootstrap.ts +5 -2
  9. package/src/compact-coordinator.ts +454 -0
  10. package/src/compact-economics-core.ts +396 -0
  11. package/src/config-catalog-core.ts +452 -0
  12. package/src/config-command.ts +181 -0
  13. package/src/config-guide.ts +361 -0
  14. package/src/core/grid-shape.ts +4 -2
  15. package/src/core/heat-plan.ts +160 -0
  16. package/src/core/observation.ts +555 -0
  17. package/src/core/subagent.ts +187 -393
  18. package/src/core/terminal.ts +121 -47
  19. package/src/core/todo.ts +25 -11
  20. package/src/dashboard-command.ts +89 -0
  21. package/src/efficiency-config-core.ts +661 -0
  22. package/src/efficiency-config-io.ts +53 -0
  23. package/src/efficiency-store.ts +312 -0
  24. package/src/focus-poller.ts +244 -0
  25. package/src/gc-core.ts +85 -0
  26. package/src/git-adapter.ts +12 -3
  27. package/src/herdr-client.ts +292 -36
  28. package/src/index-locks.ts +27 -9
  29. package/src/index-master.ts +5 -1
  30. package/src/index-notices.ts +13 -2
  31. package/src/index-pipe.ts +5 -0
  32. package/src/index-worker.ts +3 -1
  33. package/src/index.ts +330 -34
  34. package/src/jev-client.ts +146 -0
  35. package/src/jev-core.ts +381 -0
  36. package/src/lock-core.ts +105 -16
  37. package/src/observation-core.ts +248 -0
  38. package/src/pier-options.ts +96 -0
  39. package/src/pipe-channel.ts +52 -7
  40. package/src/reducer-core.ts +358 -0
  41. package/src/reducer-invoker.ts +392 -0
  42. package/src/renderers.ts +198 -0
  43. package/src/role-loader.ts +2 -1
  44. package/src/role-manifest.ts +0 -4
  45. package/src/runtime-policy.ts +5 -8
  46. package/src/session-tail.ts +60 -5
  47. package/src/settle-wake-core.ts +3 -1
  48. package/src/slim-frame.ts +4 -2
  49. package/src/storage-layout.ts +23 -0
  50. package/src/subagent-core.ts +3 -1
  51. package/src/subagent-execute.ts +328 -0
  52. package/src/subagent-gc.ts +33 -13
  53. package/src/subagent-launch.ts +4 -0
  54. package/src/subagent-list-action.ts +69 -0
  55. package/src/subagent-output-action.ts +104 -0
  56. package/src/subagent-output-adapter.ts +27 -0
  57. package/src/subagent-output-core.ts +263 -0
  58. package/src/subagent-poll-loop.ts +59 -39
  59. package/src/subagent-poller.ts +46 -0
  60. package/src/subagent-ready.ts +89 -0
  61. package/src/subagent-registry.ts +70 -0
  62. package/src/subagent-resolution.ts +41 -0
  63. package/src/subagent-scope.ts +3 -1
  64. package/src/subagent-session-io.ts +115 -32
  65. package/src/subagent-spawn.ts +153 -8
  66. package/src/swallow.ts +75 -0
  67. package/src/terminal-core.ts +82 -10
  68. package/src/todo-core.ts +5 -4
  69. package/src/todo-reminder-core.ts +8 -1
  70. package/src/todos-service.ts +18 -6
  71. package/src/tool-error.ts +26 -0
  72. package/src/vocab.ts +7 -4
package/src/todo-core.ts CHANGED
@@ -171,8 +171,9 @@ export function revertedCompleted(prev: readonly TodoItem[], next: readonly Todo
171
171
  }
172
172
 
173
173
  /** Why: Preserve the established compatibility and safety behavior. */
174
- export function makeSnapshot(items: TodoItem[]): TodoSnapshot {
175
- return { version: 1, items };
174
+ export function makeSnapshot(items: readonly TodoItem[]): TodoSnapshot {
175
+ // Copy: the snapshot is persisted, the service list stays owned by TodosService.
176
+ return { version: 1, items: [...items] };
176
177
  }
177
178
 
178
179
  export interface BranchEntryLike {
@@ -263,7 +264,7 @@ export function foldLatestTodosMeta(entries: readonly BranchEntryLike[]): Folded
263
264
  let writtenAt: number | null = null;
264
265
  for (const entry of entries) {
265
266
  if (entry.type === 'message') {
266
- const msg = entry.message;
267
+ const msg = entry.message as { role?: unknown; toolName?: unknown; details?: unknown } | null | undefined;
267
268
  if (typeof msg !== 'object' || msg === null) continue;
268
269
  if (msg.role !== 'toolResult' || msg.toolName !== TODO_TOOL_NAME) continue;
269
270
  const snapshot = extractSnapshotFromDetails(msg.details);
@@ -338,7 +339,7 @@ export function boundedView(items: readonly TodoItem[], budget: number): Bounded
338
339
  }
339
340
 
340
341
  /** Why: Preserve the established compatibility and safety behavior. */
341
- export function currentActivity(items: TodoItem[]): string | null {
342
+ export function currentActivity(items: readonly TodoItem[]): string | null {
342
343
  return items.find((it) => it.status === 'in_progress')?.content ?? null;
343
344
  }
344
345
 
@@ -17,6 +17,7 @@
17
17
  * running, a human gate is blocked, or no open items remain.
18
18
  */
19
19
  import { ABORT_STOP_REASON } from './settle-wake-core.ts';
20
+ import { pierOption } from './pier-options.ts';
20
21
 
21
22
  /** Cap reminders for the lifetime of the process, matching the previous behavior. */
22
23
  export const TODO_REMINDERS_MAX = 3;
@@ -31,12 +32,17 @@ export const TODO_REMINDER_CUSTOM_TYPE = 'pi-herdr.todo-reminder';
31
32
  * can use a small value rather than freezing configuration at module load.
32
33
  */
33
34
  export function todoReminderGraceMs(): number {
34
- return Number(process.env.PI_HERDR_TODO_GRACE_MS ?? 30_000) || 30_000;
35
+ // B10: canonical PIER_TODO_GRACE_MS, legacy PI_HERDR_TODO_GRACE_MS.
36
+ return Number(pierOption('PIER_TODO_GRACE_MS') ?? 30_000) || 30_000;
35
37
  }
36
38
 
37
39
  export interface TodoReminderInput {
38
40
  /** Stop reason of the last assistant turn before this settlement (null means treat it as natural completion). */
39
41
  lastStopReason: string | null;
42
+ /** True when abort was triggered intentionally by internal mechanisms like OCC. */
43
+ intentionalAbort?: boolean;
44
+ /** True when compaction is currently in-flight. */
45
+ compactionInFlight?: boolean;
40
46
  /** Number of reminders already injected. */
41
47
  reminders: number;
42
48
  /** Number of running background subagents; a positive value means the master is already waiting. */
@@ -67,6 +73,7 @@ function noInject(reminders: number): TodoReminderPlan {
67
73
  * (the missing guard in the 01a040cc incident).
68
74
  */
69
75
  export function planStopTodoReminder(input: TodoReminderInput): TodoReminderPlan {
76
+ if (input.intentionalAbort || input.compactionInFlight) return noInject(input.reminders);
70
77
  if (input.lastStopReason === ABORT_STOP_REASON) return noInject(input.reminders);
71
78
  if (input.reminders >= TODO_REMINDERS_MAX) return noInject(input.reminders);
72
79
  if (input.runningSubs > 0) return noInject(input.reminders);
@@ -15,6 +15,14 @@ export interface TodosConfig {
15
15
  allowParallelInProgress: boolean;
16
16
  }
17
17
 
18
+ export type TodoCompletionSource = 'tool' | 'reconcile' | 'human' | 'archive';
19
+
20
+ export interface TodoCompletedEvent {
21
+ count: number;
22
+ at: number;
23
+ source: TodoCompletionSource;
24
+ }
25
+
18
26
  export class TodosService extends EventEmitter {
19
27
  private _items: TodoItem[] = [];
20
28
  /**
@@ -45,33 +53,37 @@ export class TodosService extends EventEmitter {
45
53
  return this._items.map((it) => ({ ...it }));
46
54
  }
47
55
 
48
- replace(items: readonly TodoItem[]): { changed: boolean } {
56
+ replace(items: readonly TodoItem[], opts?: { source?: TodoCompletionSource }): { changed: boolean } {
49
57
  const next = items.map((it) => ({ ...it }));
50
58
  if (listsEqual(this._items, next)) return { changed: false };
51
59
  const before = this._items;
52
60
  this._items = next;
53
61
  this.lastWriteAt = Date.now();
54
- this.emitCompletedTransitions(before, this._items);
62
+ this.emitCompletedTransitions(before, this._items, opts?.source ?? 'tool');
55
63
  this.emit('todo.updated', { items: this._items });
56
64
  return { changed: true };
57
65
  }
58
66
 
59
- applyEdits(edits: readonly TodoEdit[]): { changed: boolean } {
67
+ applyEdits(edits: readonly TodoEdit[], opts?: { source?: TodoCompletionSource }): { changed: boolean } {
60
68
  const before = this._items;
61
69
  const next = applyTodoEdits(this._items, edits);
62
70
  if (listsEqual(before, next)) return { changed: false };
63
71
  this._items = next;
64
72
  this.lastWriteAt = Date.now();
65
- this.emitCompletedTransitions(before, this._items);
73
+ this.emitCompletedTransitions(before, this._items, opts?.source ?? 'reconcile');
66
74
  this.emit('todo.edited', { edits, items: this._items });
67
75
  this.emit('todo.updated', { items: this._items });
68
76
  return { changed: true };
69
77
  }
70
78
 
71
79
  /** Why: Preserve the established compatibility and safety behavior (M16). */
72
- private emitCompletedTransitions(before: readonly TodoItem[], after: readonly TodoItem[]): void {
80
+ private emitCompletedTransitions(
81
+ before: readonly TodoItem[],
82
+ after: readonly TodoItem[],
83
+ source: TodoCompletionSource = 'tool',
84
+ ): void {
73
85
  const count = countCompletedTransitions(before, after);
74
- if (count > 0) this.emit('todo.completed', { count, at: Date.now() });
86
+ if (count > 0) this.emit('todo.completed', { count, at: Date.now(), source });
75
87
  }
76
88
 
77
89
  rebuild(entries: readonly unknown[]): void {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A1: pi only marks a tool result as failed when `execute()` throws — "Returning a value never sets
3
+ * the error flag regardless of what properties you include in the return object"
4
+ * (pi docs/extensions.md, "Signaling errors").
5
+ *
6
+ * The round-2 audit found 20+ real failures in pier's tools returned as plain text, so neither the
7
+ * model nor pi's own hooks keyed on `event.isError` could see them (pier's B1 "subagent alive but
8
+ * reported no output" self-heal was dead code for exactly that reason). Every hard failure now goes
9
+ * through `toolError()`; deliberate non-failures (empty result set, "no match within Xms", a pane
10
+ * that simply has not produced output yet) stay normal results.
11
+ *
12
+ * The "Error: " prefix the old text returns carried is dropped: the provider's tool-result error
13
+ * channel and pi's red rendering already mark it, and keeping it would double the prefix once pi
14
+ * formats a thrown error.
15
+ */
16
+ export class ToolError extends Error {
17
+ constructor(message: string) {
18
+ super(String(message).replace(/^Error:\s*/, ''));
19
+ this.name = 'ToolError';
20
+ }
21
+ }
22
+
23
+ /** Throw a tool failure (never returns). */
24
+ export function toolError(message: string): never {
25
+ throw new ToolError(message);
26
+ }
package/src/vocab.ts CHANGED
@@ -48,13 +48,16 @@ export const TODO_TOOL_NAME = 'todo_write';
48
48
  /** Snapshot key in tool-result details (persisted in session JSONL). */
49
49
  export const TODO_DETAILS_KEY = 'pi-herdr.todo';
50
50
 
51
- /** Confirmation copy (byte-aligned with DSH so model vocab transfers). */
52
- export function formatTodoConfirmation(items: TodoItem[]): string {
51
+ /** Confirmation copy (byte-aligned with DSH so model vocab transfers; blocked is appended only when present). */
52
+ export function formatTodoConfirmation(items: readonly TodoItem[]): string {
53
53
  const c = countTodos(items);
54
- return `Updated todo list: ${c.pending} pending, ${c.inProgress} in progress, ${c.completed} completed.`;
54
+ const base = `Updated todo list: ${c.pending} pending, ${c.inProgress} in progress, ${c.completed} completed.`;
55
+ // A4: blocked items used to be invisible here (the model read "nothing pending" while items sat
56
+ // blocked waiting on a human). Appending only when non-zero keeps the DSH-aligned wording intact.
57
+ return c.blocked > 0 ? `${base.slice(0, -1)}, ${c.blocked} blocked.` : base;
55
58
  }
56
59
 
57
- export function countTodos(items: TodoItem[]): TodoCounts {
60
+ export function countTodos(items: readonly TodoItem[]): TodoCounts {
58
61
  let pending = 0;
59
62
  let inProgress = 0;
60
63
  let completed = 0;