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.
- package/README.md +20 -7
- package/package.json +7 -2
- package/schemas/efficiency-config.schema.json +77 -0
- package/skills/pi-herdr-workflow/SKILL.md +12 -0
- package/src/ansi-text.ts +83 -0
- package/src/ask-multi.ts +260 -0
- package/src/ask-user.ts +60 -10
- package/src/bootstrap.ts +5 -2
- package/src/compact-coordinator.ts +454 -0
- package/src/compact-economics-core.ts +396 -0
- package/src/config-catalog-core.ts +452 -0
- package/src/config-command.ts +181 -0
- package/src/config-guide.ts +361 -0
- package/src/core/grid-shape.ts +4 -2
- package/src/core/heat-plan.ts +160 -0
- package/src/core/observation.ts +555 -0
- package/src/core/subagent.ts +187 -393
- package/src/core/terminal.ts +121 -47
- package/src/core/todo.ts +25 -11
- package/src/dashboard-command.ts +89 -0
- package/src/efficiency-config-core.ts +661 -0
- package/src/efficiency-config-io.ts +53 -0
- package/src/efficiency-store.ts +312 -0
- package/src/focus-poller.ts +244 -0
- package/src/gc-core.ts +85 -0
- package/src/git-adapter.ts +12 -3
- package/src/herdr-client.ts +292 -36
- package/src/index-locks.ts +27 -9
- package/src/index-master.ts +5 -1
- package/src/index-notices.ts +13 -2
- package/src/index-pipe.ts +5 -0
- package/src/index-worker.ts +3 -1
- package/src/index.ts +330 -34
- package/src/jev-client.ts +146 -0
- package/src/jev-core.ts +381 -0
- package/src/lock-core.ts +105 -16
- package/src/observation-core.ts +248 -0
- package/src/pier-options.ts +96 -0
- package/src/pipe-channel.ts +52 -7
- package/src/reducer-core.ts +358 -0
- package/src/reducer-invoker.ts +392 -0
- package/src/renderers.ts +198 -0
- package/src/role-loader.ts +2 -1
- package/src/role-manifest.ts +0 -4
- package/src/runtime-policy.ts +5 -8
- package/src/session-tail.ts +60 -5
- package/src/settle-wake-core.ts +3 -1
- package/src/slim-frame.ts +4 -2
- package/src/storage-layout.ts +23 -0
- package/src/subagent-core.ts +3 -1
- package/src/subagent-execute.ts +328 -0
- package/src/subagent-gc.ts +33 -13
- package/src/subagent-launch.ts +4 -0
- package/src/subagent-list-action.ts +69 -0
- package/src/subagent-output-action.ts +104 -0
- package/src/subagent-output-adapter.ts +27 -0
- package/src/subagent-output-core.ts +263 -0
- package/src/subagent-poll-loop.ts +59 -39
- package/src/subagent-poller.ts +46 -0
- package/src/subagent-ready.ts +89 -0
- package/src/subagent-registry.ts +70 -0
- package/src/subagent-resolution.ts +41 -0
- package/src/subagent-scope.ts +3 -1
- package/src/subagent-session-io.ts +115 -32
- package/src/subagent-spawn.ts +153 -8
- package/src/swallow.ts +75 -0
- package/src/terminal-core.ts +82 -10
- package/src/todo-core.ts +5 -4
- package/src/todo-reminder-core.ts +8 -1
- package/src/todos-service.ts +18 -6
- package/src/tool-error.ts +26 -0
- 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
|
-
|
|
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
|
-
|
|
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);
|
package/src/todos-service.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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;
|