pi-harness-runtime 1.1.459 → 1.1.460

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 (45) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/harness/pending-countdown.ts +348 -0
  3. package/index.ts +216 -28
  4. package/package.json +1 -1
  5. package/packages/agent-policy/package.json +1 -1
  6. package/packages/auth/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/checkpoint/package.json +1 -1
  9. package/packages/clipboard/package.json +1 -1
  10. package/packages/codex-watcher/package.json +1 -1
  11. package/packages/config-capture/package.json +1 -1
  12. package/packages/contracts/package.json +1 -1
  13. package/packages/cookie-sanitizer/package.json +1 -1
  14. package/packages/evaluation-engine/package.json +1 -1
  15. package/packages/event-bus/package.json +1 -1
  16. package/packages/event-store/package.json +1 -1
  17. package/packages/experience-replay/package.json +1 -1
  18. package/packages/file-copy-helper/package.json +1 -1
  19. package/packages/intent-analyzer/package.json +1 -1
  20. package/packages/jev-judge/package.json +1 -1
  21. package/packages/knowledge-retrieval/package.json +1 -1
  22. package/packages/learning-engine/package.json +1 -1
  23. package/packages/logging/package.json +1 -1
  24. package/packages/memory-engine/package.json +1 -1
  25. package/packages/notification/package.json +1 -1
  26. package/packages/paperclip-integration/package.json +1 -1
  27. package/packages/performance-optimizer/package.json +1 -1
  28. package/packages/privilege-broker/package.json +1 -1
  29. package/packages/prompt-compiler/package.json +1 -1
  30. package/packages/providers/package.json +1 -1
  31. package/packages/qdrant-skills/package.json +1 -1
  32. package/packages/quota-manager/package.json +1 -1
  33. package/packages/relay/package.json +1 -1
  34. package/packages/skill-mcp-client/package.json +1 -1
  35. package/packages/skills/package.json +1 -1
  36. package/packages/todo-bd-sync/package.json +1 -1
  37. package/packages/token-estimation/package.json +1 -1
  38. package/packages/tooling-preference/package.json +1 -1
  39. package/packages/trajectory/package.json +1 -1
  40. package/packages/tui/package.json +1 -1
  41. package/packages/two-phase-review/package.json +1 -1
  42. package/packages/types/package.json +1 -1
  43. package/packages/webmcp-tester/package.json +1 -1
  44. package/packages/wiki/package.json +1 -1
  45. package/packages/write-review/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [1.1.460](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.448...v1.1.460) (2026-10-28)
6
+
7
+
8
+ ### Features
9
+
10
+ * **delegation:** a child is only a child when it really ran ([ac4e678](https://github.com/ManotLuijiu/pi-harness-runtime/commit/ac4e6782d7d4bc4941e458425c8a9f38a51fc9d4))
11
+ * **delegation:** the scope of a request is measured before it is judged ([684b6a5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/684b6a55184f63d3be24c48dcecfb0b13410dc15))
12
+ * **honcho:** the memory service speaks the native MCP of Pi ([f9f740c](https://github.com/ManotLuijiu/pi-harness-runtime/commit/f9f740c12869bae4c4380afbe8c76e12388bb744))
13
+ * **jev:** the pending question carries the ask and the answer ([a792133](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a792133e517fb8c12d3363e98942f3af30270f27))
14
+ * **jev:** the pending question counts down to the decision ([d0cc171](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d0cc171223986b2d32c670b6c1c74712fe0a4ad8))
15
+ * **keys:** the key files are repaired when their permissions are unsafe ([e9c2c26](https://github.com/ManotLuijiu/pi-harness-runtime/commit/e9c2c26b287665b56ed749de269737e8403bb8af))
16
+ * **merge:** a manual run can leave the merge to the pull request event ([b3d1950](https://github.com/ManotLuijiu/pi-harness-runtime/commit/b3d1950b5941c78ccd56951acdafd1d4afbe1bff))
17
+ * **phase:** the agent owns the todo list and the harness says so ([c489370](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c489370bd7926aece8cb1a77aa20ca2d2672f1fb))
18
+ * **review:** the review starts on its own when the implementation is done ([04f51f5](https://github.com/ManotLuijiu/pi-harness-runtime/commit/04f51f599cd3222b6c74dd8fadb1373bd72d9bd7))
19
+ * **status:** the status line names the phase, the progress and the children ([0167bf3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/0167bf396a0b987c1d44f3b869351976a951dd65))
20
+ * **test:** the task waits for the final tier of the validation plan ([fa147db](https://github.com/ManotLuijiu/pi-harness-runtime/commit/fa147dbf7d50d56e6ea48c6d8fbb4fcb190caf8a))
21
+ * **validation:** a large job starts with a measured pilot ([425964b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/425964b04a3d4fa02adfabed47ff62c5ec2b9c28))
22
+ * **validation:** the size of a job is a fact, not a phrase ([8aa6c11](https://github.com/ManotLuijiu/pi-harness-runtime/commit/8aa6c119565d0f8412dc8aeafa6b295d47d3f093))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **ci:** every run of a required check counts, and the worst one decides ([da9e0da](https://github.com/ManotLuijiu/pi-harness-runtime/commit/da9e0da6b0ca29cb320764fd5fc1b8a8dee8b1e0))
28
+ * **ci:** the newest attempt of a run decides, not the attempt that failed ([9d7adcc](https://github.com/ManotLuijiu/pi-harness-runtime/commit/9d7adcc31cbacc517c3c1a9bcd3956be94c908ef))
29
+ * **hooks:** a tag is checked through the commit behind it ([c823e8e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/c823e8e1f6973c62b5449a97c101759a4d5cbba5))
30
+ * **jev:** the alternatives of a question are parsed without a quadratic pattern ([63016a3](https://github.com/ManotLuijiu/pi-harness-runtime/commit/63016a3f0488acb801990dd46770c00d382b5858))
31
+ * **keys:** the key directory is audited as a tree ([081cc75](https://github.com/ManotLuijiu/pi-harness-runtime/commit/081cc75ac8ceff36b0d619c3f76371dc08459d3c))
32
+ * **merge:** the boolean input is read as a boolean ([971b716](https://github.com/ManotLuijiu/pi-harness-runtime/commit/971b7169a43db382dbdeb6bb9bab02ab56e0351c))
33
+ * **merge:** the manual job needs the right of the merge ([7ba685e](https://github.com/ManotLuijiu/pi-harness-runtime/commit/7ba685ec43d815f90aa35b742579e87e76c69209))
34
+ * **merge:** the merge of the pull request is the GraphQL mutation ([972bab4](https://github.com/ManotLuijiu/pi-harness-runtime/commit/972bab4b0f7faf1b0047f3bb767ef6db3de4b2fa))
35
+ * **merge:** the workflow merges the pull request the gates cleared ([d7639ec](https://github.com/ManotLuijiu/pi-harness-runtime/commit/d7639ec9b83723510acb4a5c4e23d5d6167c3af6))
36
+ * **release:** a manual release keeps the version of its manifest ([122e661](https://github.com/ManotLuijiu/pi-harness-runtime/commit/122e661ea48cb8aff14c71dbd0221a4cf0ba4c8e))
37
+ * **release:** the alignment names the reason of a failure ([5814dd0](https://github.com/ManotLuijiu/pi-harness-runtime/commit/5814dd0b780f5839cf8011ed0cbbbb31e7eb0418))
38
+ * **release:** the channels of the package name the release ([a7a273f](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a7a273ffe8398e2b5b07ef1c321777de41792c6e))
39
+ * **release:** the release publishes and leaves the channels alone ([5871746](https://github.com/ManotLuijiu/pi-harness-runtime/commit/5871746589c1281d855dbeb591b02419fa43f22e))
40
+ * **release:** the release pull request names the version of its branch ([a846e83](https://github.com/ManotLuijiu/pi-harness-runtime/commit/a846e83e6de6e529f4bca05e869db0b5eebb37c5))
41
+ * **release:** the release scans the tree of the commit it publishes ([7d47a0b](https://github.com/ManotLuijiu/pi-harness-runtime/commit/7d47a0b5406480a1e1d3a52623a76aabd1e2d0ba))
42
+
5
43
  ### [1.1.459](https://github.com/ManotLuijiu/pi-harness-runtime/compare/v1.1.448...v1.1.459) (2026-10-28)
6
44
 
7
45
 
@@ -0,0 +1,348 @@
1
+ /**
2
+ * The live countdown of a pending question.
3
+ *
4
+ * The harness asks a human a question and waits. The wait has a deadline,
5
+ * and the countdown shows the remaining time in the status row of the TUI
6
+ * and starts the decision of the judge exactly at that deadline.
7
+ *
8
+ * The module owns the two policies of the wait:
9
+ *
10
+ * - the first countdown counts down to the deadline of the decision, which
11
+ * is derived from the moment the wait started,
12
+ * - the recheck counts down to the next attempt, after the judge asked to
13
+ * wait longer.
14
+ *
15
+ * The caller cannot pass a deadline of its own: the displayed deadline and
16
+ * the deadline the judge uses therefore cannot drift apart.
17
+ *
18
+ * The countdown is bound to one request. Every start replaces the previous
19
+ * binding and every stop releases the timer, so there is never more than
20
+ * one countdown and never a timer without a request.
21
+ */
22
+
23
+ /** What the countdown counts to. */
24
+ export type CountdownMode = "initial" | "recheck";
25
+
26
+ /** The state of the countdown that is drawn in the status row. */
27
+ export interface CountdownView {
28
+ /** The request the countdown belongs to. */
29
+ requestId: string;
30
+ /** The milliseconds that are left. Never negative. */
31
+ remainingMs: number;
32
+ /** The remaining time as "MM:SS". */
33
+ remaining: string;
34
+ /** What the countdown counts to. */
35
+ mode: CountdownMode;
36
+ }
37
+
38
+ /** The dependencies of a countdown. */
39
+ export interface PendingCountdownDeps {
40
+ /** Draw the countdown in the status row. */
41
+ render: (view: CountdownView) => void;
42
+ /** The deadline was reached and the decision is due. */
43
+ onDeadline: (requestId: string) => void;
44
+ /**
45
+ * Is the request still the pending one?
46
+ *
47
+ * A tick of a question that was answered or replaced in the meantime
48
+ * must not redraw its status and must not trigger a decision.
49
+ */
50
+ isPending: (requestId: string) => boolean;
51
+ /** A failing callback of the countdown is reported here. */
52
+ onError?: (error: unknown) => void;
53
+ /** The wait is over: the status row is cleared. */
54
+ onStop?: (requestId: string | null) => void;
55
+ /** The minutes the judge waits for a human answer. */
56
+ waitMinutes: number;
57
+ /** The milliseconds until the next attempt after the judge asked to wait. */
58
+ recheckIntervalMs: number;
59
+ /** The clock (the tests drive it). */
60
+ now?: () => number;
61
+ /** The timer that drives the ticks (the tests drive it). */
62
+ setInterval?: (callback: () => void, ms: number) => unknown;
63
+ /** The timer that stops the ticks (the tests drive it). */
64
+ clearInterval?: (handle: unknown) => void;
65
+ /** The tick interval in milliseconds. */
66
+ tickMs?: number;
67
+ }
68
+
69
+ /** The milliseconds between two ticks of the display. */
70
+ const COUNTDOWN_TICK_MS = 1_000;
71
+
72
+ /**
73
+ * The smallest tick interval.
74
+ *
75
+ * A shorter interval would repaint the status row faster than a reader can
76
+ * follow it and would waste the event loop of the harness.
77
+ */
78
+ const MIN_TICK_MS = 100;
79
+
80
+ /**
81
+ * The deadline of the decision.
82
+ *
83
+ * The deadline is derived from the moment the wait started, so the
84
+ * countdown and the judge share one timestamp.
85
+ */
86
+ export function decisionDeadlineAt(startedAt: number, waitMinutes: number): number {
87
+ const minutes = Number.isFinite(waitMinutes) ? Math.max(0, waitMinutes) : 0;
88
+ const started = Number.isFinite(startedAt) ? startedAt : 0;
89
+ return started + minutes * 60_000;
90
+ }
91
+
92
+ /**
93
+ * The remaining time as "MM:SS".
94
+ *
95
+ * A time that is not a number is shown as an empty countdown: a status row
96
+ * must never claim a time that does not exist.
97
+ */
98
+ export function formatRemainingTime(remainingMs: number): string {
99
+ const safeMs = Number.isFinite(remainingMs) ? Math.max(0, remainingMs) : 0;
100
+ const totalSeconds = Math.floor(safeMs / 1_000);
101
+ const minutes = Math.floor(totalSeconds / 60);
102
+ const seconds = totalSeconds % 60;
103
+ return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
104
+ }
105
+
106
+ /** The binding of a running countdown. */
107
+ interface CountdownBinding {
108
+ requestId: string;
109
+ deadlineAt: number;
110
+ mode: CountdownMode;
111
+ }
112
+
113
+ /**
114
+ * The countdown of one pending question.
115
+ *
116
+ * The countdown renders at once: the status row must show the remaining
117
+ * time without waiting for the first tick.
118
+ */
119
+ export class PendingQuestionCountdown {
120
+ private readonly deps: PendingCountdownDeps;
121
+ private readonly now: () => number;
122
+ private readonly startTimer: (callback: () => void, ms: number) => unknown;
123
+ private readonly stopTimer: (handle: unknown) => void;
124
+ private readonly tickMs: number;
125
+ private handle: unknown = null;
126
+ private binding: CountdownBinding | null = null;
127
+ /**
128
+ * Bumped on every start and every stop.
129
+ *
130
+ * A tick of an older generation belongs to a binding that is gone: it is
131
+ * ignored instead of redrawing a status or triggering a decision.
132
+ */
133
+ private generation = 0;
134
+ /**
135
+ * True while the countdown owns the wait.
136
+ *
137
+ * The countdown stays engaged after its deadline fired, until the
138
+ * decision path stops it or replaces it. The fallback poll uses this to
139
+ * stay out of the way of the accurate timer.
140
+ */
141
+ private engaged = false;
142
+ /**
143
+ * True when the deadline fired and the decision did not start yet.
144
+ *
145
+ * The countdown owns the wait in that state, but it has no timer: the
146
+ * caller arms it again when the decision did not start.
147
+ */
148
+ private deadlineFired = false;
149
+
150
+ constructor(deps: PendingCountdownDeps) {
151
+ this.deps = deps;
152
+ this.now = deps.now ?? (() => Date.now());
153
+ this.startTimer =
154
+ deps.setInterval ??
155
+ ((callback, ms) => setInterval(callback, ms) as unknown);
156
+ this.stopTimer =
157
+ deps.clearInterval ??
158
+ ((handle) => clearInterval(handle as NodeJS.Timeout));
159
+ this.tickMs = Math.max(MIN_TICK_MS, deps.tickMs ?? COUNTDOWN_TICK_MS);
160
+ }
161
+
162
+ /** Does the countdown own the current wait? */
163
+ get active(): boolean {
164
+ return this.engaged;
165
+ }
166
+
167
+ /** Did the deadline fire and is the decision still due? */
168
+ get fired(): boolean {
169
+ return this.deadlineFired;
170
+ }
171
+
172
+ /** The request the countdown belongs to. */
173
+ get requestId(): string | null {
174
+ return this.binding?.requestId ?? null;
175
+ }
176
+
177
+ /** What the countdown counts to. */
178
+ get mode(): CountdownMode | null {
179
+ return this.binding?.mode ?? null;
180
+ }
181
+
182
+ /**
183
+ * Start the countdown of a request, or replace the running one.
184
+ *
185
+ * The deadline of the decision is derived from the moment the wait
186
+ * started: the caller passes that timestamp, the countdown computes the
187
+ * deadline. The countdown and the judge therefore share one timestamp.
188
+ */
189
+ start(requestId: string, startedAt: number): void {
190
+ this.begin(requestId, decisionDeadlineAt(startedAt, this.deps.waitMinutes), "initial");
191
+ }
192
+
193
+ /**
194
+ * Start the countdown of the next attempt.
195
+ *
196
+ * The judge asked to wait longer: the countdown counts down to the next
197
+ * attempt instead of to the deadline of the decision.
198
+ */
199
+ startRecheck(requestId: string): void {
200
+ this.begin(requestId, this.now() + this.deps.recheckIntervalMs, "recheck");
201
+ }
202
+
203
+ /**
204
+ * Stop the countdown and release its timer.
205
+ *
206
+ * Stopping is idempotent: a countdown that is not running can be stopped
207
+ * as often as the cleanup paths need. The status row is cleared with it,
208
+ * so a dead countdown never leaves a row that claims a decision is due.
209
+ */
210
+ stop(): void {
211
+ const requestId = this.binding?.requestId ?? null;
212
+ this.engaged = false;
213
+ this.deadlineFired = false;
214
+ this.generation += 1;
215
+ this.clearTimer();
216
+ this.binding = null;
217
+ this.safeCall(() => this.deps.onStop?.(requestId));
218
+ }
219
+
220
+ /** Bind the countdown to a request and start its timer. */
221
+ private begin(requestId: string, deadlineAt: number, mode: CountdownMode): void {
222
+ this.clearTimer();
223
+ this.generation += 1;
224
+ const generation = this.generation;
225
+ this.binding = { requestId, deadlineAt, mode };
226
+ this.engaged = true;
227
+ this.deadlineFired = false;
228
+ try {
229
+ this.handle = this.startTimer(() => this.tick(generation), this.tickMs);
230
+ } catch (error) {
231
+ // The timer could not be created: the countdown owns nothing, so
232
+ // it must not claim the wait. The row of the countdown is
233
+ // cleared with it, so no row claims a decision that is not due.
234
+ this.engaged = false;
235
+ this.binding = null;
236
+ this.safeCall(() => this.deps.onStop?.(requestId));
237
+ this.report(error);
238
+ return;
239
+ }
240
+ this.tick(generation);
241
+ }
242
+
243
+ /**
244
+ * One tick of the countdown.
245
+ *
246
+ * The tick is a no-op when it belongs to a binding that is gone, or
247
+ * when the request it counts for is no longer the pending one.
248
+ */
249
+ private tick(generation: number): void {
250
+ if (!this.engaged || generation !== this.generation) return;
251
+ const binding = this.binding;
252
+ if (!binding) return;
253
+
254
+ let remainingMs: number;
255
+ try {
256
+ if (!this.deps.isPending(binding.requestId)) {
257
+ // The question was answered or replaced: the countdown of
258
+ // that question is over.
259
+ this.stop();
260
+ return;
261
+ }
262
+ // The callback may have stopped the countdown: the binding of
263
+ // this tick is gone and must not be drawn any more.
264
+ if (!this.engaged || generation !== this.generation) return;
265
+ remainingMs = Math.max(0, binding.deadlineAt - this.now());
266
+ } catch (error) {
267
+ // A failing callback must never escape the timer: the countdown
268
+ // stops and the failure is reported.
269
+ this.stop();
270
+ this.report(error);
271
+ return;
272
+ }
273
+
274
+ if (remainingMs <= 0) {
275
+ // The deadline is reached. The timer stops, the countdown stays
276
+ // engaged until the decision path stops it or replaces it, so
277
+ // the fallback poll does not start a second decision.
278
+ this.clearTimer();
279
+ this.deadlineFired = true;
280
+ // The countdown reaches its zero before the decision starts.
281
+ this.safeRender({
282
+ requestId: binding.requestId,
283
+ remainingMs: 0,
284
+ remaining: formatRemainingTime(0),
285
+ mode: binding.mode,
286
+ });
287
+ // A failing decision handler must not leave a countdown that owns
288
+ // the wait without a timer: the countdown stops, so the fallback
289
+ // poll can take over.
290
+ try {
291
+ this.deps.onDeadline(binding.requestId);
292
+ } catch (error) {
293
+ this.stop();
294
+ this.report(error);
295
+ }
296
+ return;
297
+ }
298
+
299
+ this.safeRender({
300
+ requestId: binding.requestId,
301
+ remainingMs,
302
+ remaining: formatRemainingTime(remainingMs),
303
+ mode: binding.mode,
304
+ });
305
+ }
306
+
307
+ /** Draw the countdown, and report a failing status row. */
308
+ private safeRender(view: CountdownView): void {
309
+ try {
310
+ this.deps.render(view);
311
+ } catch (error) {
312
+ this.stop();
313
+ this.report(error);
314
+ }
315
+ }
316
+
317
+ /** Run a callback of the countdown without letting it escape. */
318
+ private safeCall(action: () => void): void {
319
+ try {
320
+ action();
321
+ } catch (error) {
322
+ this.report(error);
323
+ }
324
+ }
325
+
326
+ /** Report a failure without letting the report escape. */
327
+ private report(error: unknown): void {
328
+ try {
329
+ this.deps.onError?.(error);
330
+ } catch {
331
+ // The report of a failure must not become a new failure.
332
+ }
333
+ }
334
+
335
+ /** Release the timer of the countdown. */
336
+ private clearTimer(): void {
337
+ if (this.handle === null) return;
338
+ const handle = this.handle;
339
+ this.handle = null;
340
+ try {
341
+ this.stopTimer(handle);
342
+ } catch (error) {
343
+ // The timer is already released from the point of view of the
344
+ // countdown: the failure is reported, nothing is left behind.
345
+ this.report(error);
346
+ }
347
+ }
348
+ }
package/index.ts CHANGED
@@ -42,6 +42,10 @@ import {
42
42
  relayShouldBeReported,
43
43
  } from "./harness/startup-report.ts";
44
44
  import { localSkillsReport, skillsReport } from "./harness/skills-startup.ts";
45
+ import {
46
+ PendingQuestionCountdown,
47
+ type CountdownView,
48
+ } from "./harness/pending-countdown.ts";
45
49
  import { isChannelDisabled, setChannelDisabled } from "./harness/notification-settings.ts";
46
50
  // The Honcho contract lives with the Honcho integration. A type-only import
47
51
  // is erased at runtime, so the startup keeps loading the module lazily.
@@ -292,6 +296,9 @@ const sharedState = {
292
296
  validationLedger: null as unknown,
293
297
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
298
  updateAutoContinueStatus: null as any,
299
+ // The live countdown of the pending question. The channels stop it when
300
+ // a human answer arrives.
301
+ pendingCountdown: null as PendingQuestionCountdown | null,
295
302
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
296
303
  pqController: null as any,
297
304
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -335,6 +342,8 @@ function applyChannelApproval(
335
342
  // cleared after a successful resolution.
336
343
  sharedState.waitingForUserSince = null;
337
344
  sharedState.updateAutoContinueStatus?.("IDLE");
345
+ // The countdown of the answered question is over with it.
346
+ sharedState.pendingCountdown?.stop();
338
347
 
339
348
  if (result.resolution === "yes") {
340
349
  (sharedState.todoContinuationController as any)?.resume?.();
@@ -932,6 +941,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
932
941
  let lastUserMessage: Date | null = null;
933
942
  let autoContinueTimer: NodeJS.Timeout | null = null;
934
943
  let _inferenceInProgress = false; // Track for overlap prevention
944
+ // The timing of the wait. The countdown in the status row and the
945
+ // decision of the judge use the same constants, so the displayed
946
+ // deadline and the decision can never drift apart.
947
+ const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Fallback poll: every minute
948
+ const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
949
+ const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
935
950
  // A continuation was dispatched and its turn has not settled yet.
936
951
  // The gate owns that state: the status stays "RUN" until the turn
937
952
  // settles, and no timer may clear it in the meantime.
@@ -1023,6 +1038,15 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1023
1038
  }
1024
1039
  });
1025
1040
  let _sessionId = Date.now().toString(36); // Track session for cancellation
1041
+ // The session of the Pi runtime. A countdown belongs to the session
1042
+ // that asked the question.
1043
+ let _piSessionId = "";
1044
+ // The session the running countdown belongs to. A countdown of a
1045
+ // session that is gone must not draw its status any more.
1046
+ let _countdownSessionId = "";
1047
+ // The request the row of the footer belongs to. A countdown that
1048
+ // stops must not clear the row of a newer question.
1049
+ let _countdownRowRequestId: string | null = null;
1026
1050
  let _lastNotifiedQuestion = ""; // Track last question to prevent repeat notifications
1027
1051
  let _completedNotifiedFor: string | null = null; // Track completion notification deduplication
1028
1052
 
@@ -1048,36 +1072,164 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1048
1072
  ERROR: "Auto-continue unavailable",
1049
1073
  } as const;
1050
1074
 
1075
+ // The live countdown of the pending question.
1076
+ //
1077
+ // It owns the deadline of the wait: it shows the remaining time in
1078
+ // the status row once per second and starts the decision of the
1079
+ // judge exactly at the deadline. The fallback poll only takes over
1080
+ // when there is no countdown, so a decision is never started twice.
1081
+ const pendingCountdown = new PendingQuestionCountdown({
1082
+ // The two policies of the wait belong to the countdown, so the
1083
+ // displayed deadline and the deadline of the judge cannot drift
1084
+ // apart.
1085
+ waitMinutes: MIN_WAIT_MINUTES,
1086
+ recheckIntervalMs: AUTO_CONTINUE_INTERVAL_MS,
1087
+ render: (view: CountdownView) => {
1088
+ _countdownRowRequestId = view.requestId;
1089
+ updateAutoContinueStatus(
1090
+ "WAITING",
1091
+ view.requestId.slice(0, 8),
1092
+ view.mode === "recheck"
1093
+ ? `Jev will recheck in ${view.remaining}`
1094
+ : `Jev decision in ${view.remaining}`,
1095
+ );
1096
+ },
1097
+ onDeadline: (requestId: string) => {
1098
+ logToFile("[auto-continue]", "The decision deadline was reached:", requestId);
1099
+ checkAndAutoContinue("countdown")
1100
+ .then(() => {
1101
+ // The decision did not start - another one was still in
1102
+ // flight, or the wait was gone before it ran. The
1103
+ // deadline is not lost: the countdown is armed again
1104
+ // and the decision follows.
1105
+ if (pendingCountdown.fired && pendingCountdown.requestId === requestId) {
1106
+ pendingCountdown.startRecheck(requestId);
1107
+ }
1108
+ })
1109
+ .catch((error) => {
1110
+ logToFile("[auto-continue]", "The decision failed:", oneLineError(error));
1111
+ // The decision did not run: the countdown releases the
1112
+ // wait, so the fallback poll can take over again.
1113
+ pendingCountdown.stop();
1114
+ });
1115
+ },
1116
+ isPending: (requestId: string) => {
1117
+ // The wait must still belong to the request: a late tick of
1118
+ // an answered or replaced question must not draw its status
1119
+ // and must not trigger a decision.
1120
+ if (!waitingForUserSince) return false;
1121
+ if (_countdownSessionId !== _piSessionId) return false;
1122
+ const current = sharedState.pqController?.getCurrentQuestion?.();
1123
+ if (!current) return false;
1124
+ return (
1125
+ current.requestId === requestId &&
1126
+ (current.state === "pending" || current.state === "evaluating")
1127
+ );
1128
+ },
1129
+ onStop: (requestId: string | null) => {
1130
+ // The countdown is over: the row is cleared with it, so no
1131
+ // row claims a decision that is not due any more. Only the row
1132
+ // of that countdown is cleared: a row of another state - the
1133
+ // judge continues, for example - belongs to that state, and
1134
+ // the row of a newer question belongs to that question.
1135
+ if (_countdownRowRequestId && requestId && _countdownRowRequestId !== requestId) {
1136
+ return;
1137
+ }
1138
+ _countdownRowRequestId = null;
1139
+ if (_currentAutoContinueState.startsWith("[WAIT]")) {
1140
+ _currentAutoContinueState = "";
1141
+ _setAutoContinueStatus?.(undefined);
1142
+ }
1143
+ },
1144
+ onError: (error: unknown) => {
1145
+ logToFile("[auto-continue]", "The countdown failed:", oneLineError(error));
1146
+ },
1147
+ });
1148
+ sharedState.pendingCountdown = pendingCountdown;
1149
+
1051
1150
  // Wire shared state for callback handlers
1052
1151
  sharedState.waitingForUserSince = waitingForUserSince;
1053
1152
  sharedState.updateAutoContinueStatus = updateAutoContinueStatus;
1054
1153
 
1055
- // Update auto-continue status in TUI footer
1056
- function updateAutoContinueStatus(state: keyof typeof AC_STATE_MESSAGES, requestId?: string): void {
1057
- if (!_setAutoContinueStatus) return;
1154
+ /**
1155
+ * Start the countdown of a pending question.
1156
+ *
1157
+ * The deadline of the decision is derived from the wait that is
1158
+ * already running, so the displayed deadline and the decision of
1159
+ * the judge can never drift apart.
1160
+ */
1161
+ function startPendingCountdown(requestId: string): void {
1162
+ _countdownSessionId = _piSessionId;
1163
+ const startedAt = waitingForUserSince?.getTime() ?? Date.now();
1164
+ pendingCountdown.start(requestId, startedAt);
1165
+ }
1166
+
1167
+ /**
1168
+ * Start the countdown of the next attempt.
1169
+ *
1170
+ * The judge asked to wait longer: the countdown counts down to the
1171
+ * next attempt instead of to the deadline of the decision.
1172
+ */
1173
+ function startRecheckCountdown(requestId: string): void {
1174
+ _countdownSessionId = _piSessionId;
1175
+ pendingCountdown.startRecheck(requestId);
1176
+ }
1058
1177
 
1178
+ // Update auto-continue status in TUI footer
1179
+ function updateAutoContinueStatus(
1180
+ state: keyof typeof AC_STATE_MESSAGES,
1181
+ requestId?: string,
1182
+ detail?: string,
1183
+ ): void {
1059
1184
  const message = AC_STATE_MESSAGES[state];
1060
1185
  if (state === "IDLE") {
1061
- // Clear status
1062
- _setAutoContinueStatus(undefined);
1063
- _currentAutoContinueState = "";
1064
- } else {
1065
- const statusText = requestId
1066
- ? `[${AC_STATE[state]}] ${message} | ${requestId}`
1067
- : `[${AC_STATE[state]}] ${message}`;
1068
-
1069
- // Only update if state changed (prevent flicker)
1070
- if (statusText !== _currentAutoContinueState) {
1071
- _setAutoContinueStatus(statusText);
1072
- _currentAutoContinueState = statusText;
1186
+ // Only the row is cleared here: the countdown belongs to the
1187
+ // wait, not to the row. A settled continuation must not stop
1188
+ // the countdown of a question that the agent asked in the
1189
+ // same turn, and it must not blank its row either.
1190
+ if (!pendingCountdown.active) {
1191
+ _currentAutoContinueState = "";
1192
+ _setAutoContinueStatus?.(undefined);
1073
1193
  }
1194
+ return;
1195
+ }
1196
+
1197
+ const parts = [`[${AC_STATE[state]}] ${message}`];
1198
+ if (requestId) parts.push(requestId);
1199
+ if (detail) parts.push(detail);
1200
+ const statusText = parts.join(" | ");
1201
+
1202
+ // Only update if state changed (prevent flicker). The state is
1203
+ // kept even without a UI binding: a session that starts without
1204
+ // a TUI shows the current state as soon as the UI is there.
1205
+ if (statusText !== _currentAutoContinueState) {
1206
+ _currentAutoContinueState = statusText;
1207
+ _setAutoContinueStatus?.(statusText);
1074
1208
  }
1075
1209
  }
1076
1210
 
1077
1211
  // Capture UI context from session_start
1078
1212
  pi.on("session_start", (_event, ctx) => {
1213
+ // The session of the runtime: the countdown of a question belongs
1214
+ // to the session that asked it.
1215
+ const sessionId = (
1216
+ ctx as { sessionManager?: { getSessionId?: () => string | undefined } }
1217
+ ).sessionManager?.getSessionId?.();
1218
+ if (typeof sessionId === "string" && sessionId) {
1219
+ if (_piSessionId && _piSessionId !== sessionId) {
1220
+ // The session changed: the countdown of the old session
1221
+ // is over with it.
1222
+ pendingCountdown.stop();
1223
+ }
1224
+ _piSessionId = sessionId;
1225
+ }
1079
1226
  if (ctx.hasUI && ctx.ui) {
1080
1227
  _setAutoContinueStatus = ctx.ui.setStatus.bind(ctx.ui, "harness-auto-continue");
1228
+ // A session without a TUI collected the state already: it is
1229
+ // shown as soon as the UI is there.
1230
+ if (_currentAutoContinueState) {
1231
+ _setAutoContinueStatus(_currentAutoContinueState);
1232
+ }
1081
1233
  }
1082
1234
 
1083
1235
  });
@@ -1090,6 +1242,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1090
1242
  autoContinueTimer = null;
1091
1243
  }
1092
1244
 
1245
+ // The countdown of the pending question is over with the session.
1246
+ pendingCountdown.stop();
1247
+
1093
1248
  // Clear UI binding and status
1094
1249
  if (_setAutoContinueStatus) {
1095
1250
  _setAutoContinueStatus(undefined);
@@ -1148,6 +1303,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1148
1303
  waitingForUserSince = null;
1149
1304
  sharedState.waitingForUserSince = null;
1150
1305
  _lastNotifiedQuestion = "";
1306
+ // The question is answered: the countdown of the wait is over.
1307
+ pendingCountdown.stop();
1151
1308
  updateAutoContinueStatus("IDLE");
1152
1309
  // Dispatch answer to Pi
1153
1310
  const message = answer === "yes" ? "yes" : "not yet";
@@ -1224,8 +1381,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1224
1381
  // Use question.requestId as the jobId for callback matching
1225
1382
  const questionJobId = question.requestId;
1226
1383
 
1227
- // Update TUI status - use short ID for display
1228
- updateAutoContinueStatus("WAITING", questionJobId.slice(0, 8));
1384
+ // The status row counts down to the decision of the judge.
1385
+ // The countdown is bound to this request, so a newer
1386
+ // question replaces it.
1387
+ startPendingCountdown(questionJobId);
1229
1388
 
1230
1389
  // Get task context for additional info
1231
1390
  const taskContext = await getTaskContext().catch(() => ({ total: 0, completed: 0, remaining: [] as string[] }));
@@ -1388,6 +1547,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1388
1547
  if (waitingForUserSince) {
1389
1548
  waitingForUserSince = null;
1390
1549
  _lastNotifiedQuestion = "";
1550
+ // The user answered in Pi: the countdown of the wait is
1551
+ // over with it.
1552
+ pendingCountdown.stop();
1391
1553
  updateAutoContinueStatus("IDLE");
1392
1554
  // Cancel pending question on user response
1393
1555
  // (User answered via Pi instead of button)
@@ -1397,18 +1559,22 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1397
1559
  });
1398
1560
 
1399
1561
  // Periodic check for auto-continue decision
1400
- const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Check every 1 minute
1401
- const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
1402
- const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
1403
1562
 
1404
- const checkAndAutoContinue = async () => {
1563
+ const checkAndAutoContinue = async (source: "countdown" | "poll" = "poll") => {
1405
1564
  // Prevent overlap if inference is already in progress
1406
1565
  if (_inferenceInProgress) {
1566
+ // A deadline that was blocked is armed again by the caller of
1567
+ // the countdown, so the decision is not lost.
1407
1568
  return;
1408
1569
  }
1409
1570
 
1410
1571
  if (!waitingForUserSince) return;
1411
1572
 
1573
+ // The countdown owns the wait while it runs: the fallback poll
1574
+ // only takes over when there is no countdown, so the decision is
1575
+ // never started twice.
1576
+ if (source === "poll" && pendingCountdown.active) return;
1577
+
1412
1578
  // Capture session ID at check time for post-inference validation
1413
1579
  const checkSessionId = _sessionId;
1414
1580
  // Use same hash algorithm as detection
@@ -1470,6 +1636,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1470
1636
  if (taskContext.total === 0 && !hasPendingQuestion) {
1471
1637
  logToFile("[auto-continue]", "No tasks and no pending questions, skipping auto-continue check");
1472
1638
  _inferenceInProgress = false;
1639
+ // The countdown is over: the fallback poll takes over the
1640
+ // next attempt.
1641
+ pendingCountdown.stop();
1473
1642
  return;
1474
1643
  }
1475
1644
 
@@ -1624,15 +1793,20 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1624
1793
  if (waitBelongsToRequest(questionContext?.requestId)) {
1625
1794
  waitingForUserSince = null;
1626
1795
  sharedState.waitingForUserSince = null;
1796
+ // The request expired: the countdown of the wait
1797
+ // is over with it.
1798
+ pendingCountdown.stop();
1627
1799
  updateAutoContinueStatus("IDLE");
1628
1800
  }
1629
1801
  } else {
1630
- // The request stays open: the next poll asks again
1631
- // until the deadline closes it.
1632
- updateAutoContinueStatus(
1633
- "WAITING",
1634
- questionContext?.requestId.slice(0, 8),
1635
- );
1802
+ // The request stays open: the countdown counts down to
1803
+ // the next check, so the retry does not wait for the
1804
+ // fallback poll.
1805
+ if (questionContext?.requestId) {
1806
+ startRecheckCountdown(questionContext.requestId);
1807
+ } else {
1808
+ updateAutoContinueStatus("WAITING");
1809
+ }
1636
1810
  }
1637
1811
  } else {
1638
1812
  // Close the question with the Jev decision. The buttons of
@@ -1669,6 +1843,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1669
1843
  waitingForUserSince = null;
1670
1844
  sharedState.waitingForUserSince = null;
1671
1845
  _lastNotifiedQuestion = "";
1846
+ // The stale decision is discarded: the countdown
1847
+ // of the wait is over with it.
1848
+ pendingCountdown.stop();
1672
1849
  updateAutoContinueStatus("IDLE");
1673
1850
  } else {
1674
1851
  logToFile(
@@ -1692,6 +1869,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1692
1869
  waitingForUserSince = null;
1693
1870
  sharedState.waitingForUserSince = null;
1694
1871
  _lastNotifiedQuestion = "";
1872
+ // The work pauses for a human: the countdown of
1873
+ // the wait is over with it.
1874
+ pendingCountdown.stop();
1695
1875
  updateAutoContinueStatus("IDLE");
1696
1876
  }
1697
1877
  return;
@@ -1725,6 +1905,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1725
1905
  waitingForUserSince = null;
1726
1906
  sharedState.waitingForUserSince = null;
1727
1907
  _lastNotifiedQuestion = "";
1908
+ // The question is closed: the countdown of the wait is over.
1909
+ pendingCountdown.stop();
1728
1910
 
1729
1911
  // The status stays "RUN" until the turn that the steer
1730
1912
  // started has settled. A timer would clear it while the
@@ -1735,6 +1917,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1735
1917
  _inferenceInProgress = false;
1736
1918
  } catch {
1737
1919
  _inferenceInProgress = false;
1920
+ // The countdown is over: the fallback poll takes over the next
1921
+ // attempt.
1922
+ pendingCountdown.stop();
1738
1923
  updateAutoContinueStatus("ERROR");
1739
1924
  addHarnessMessage("Jev", "auto-continue evaluation failed", "err", [
1740
1925
  "How to fix: Check logs at /tmp/pi-harness.log for API errors",
@@ -2108,7 +2293,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
2108
2293
  });
2109
2294
 
2110
2295
  // Start periodic check
2111
- autoContinueTimer = setInterval(checkAndAutoContinue, AUTO_CONTINUE_INTERVAL_MS);
2296
+ autoContinueTimer = setInterval(
2297
+ () => checkAndAutoContinue("poll"),
2298
+ AUTO_CONTINUE_INTERVAL_MS,
2299
+ );
2112
2300
  if (jevKeyMissing) {
2113
2301
  // Without a provider the health check cannot run. The missing-key
2114
2302
  // message (with its fix commands) stays the visible one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/codex-watcher",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Watches Codex CLI sessions and emits visible messages for PING-PONG monitoring",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/config-capture",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Auto-capture API-fetched configuration values to AGENTS.md",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "description": "RPC schema definitions for pi-harness runtime",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cookie-sanitizer",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — cookie sanitizer (provider-quota cookie file normaliser)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/evaluation-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Evaluation engine for pi-harness runtime (RFC-0057)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/event-bus",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/event-store",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/experience-replay",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Experience replay for debugging and reproducibility (RFC-0059)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/file-copy-helper",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Injects cp/sudo cp rule when user asks to mimic/copy files",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/intent-analyzer",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/jev-judge",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Jev Judge - Structured decision model for agent routing, E2E testing, and automated decisions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/knowledge-retrieval",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Knowledge retrieval via TencentDB-Agent-Memory MCP (RFC-0105)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/learning-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Learning engine for extracting lessons from runtime execution (RFC-0058)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/logging",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "description": "Centralized structured application logging with buffered Cloudflare R2 and webhook transports",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/memory-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Memory engine with OKF integration for durable knowledge management (RFC-0060)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/notification",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — notification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/paperclip-integration",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Paperclip API integration: poll agent inbox, route tasks to pi.dev, Telegram alerts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/performance-optimizer",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Performance optimization analyzer for pi-harness runtime (RFC-0056)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/privilege-broker",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Privilege Broker — capability registry and command executor for autonomous runtime (RFC-0101 §7)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/prompt-compiler",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Deterministic prompt compiler for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/providers",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — providers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/qdrant-skills",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Qdrant vector search integration for Hermes-style skill system",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/quota-manager",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — quota-manager",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/relay",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "type": "module",
5
5
  "description": "Cloud relay for remote access - T3 Connect style for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/skill-mcp-client",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "MCP client for Skills SaaS backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/skills",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "File-based skill system with pi.dev compatibility and Hermes self-improvement",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/todo-bd-sync",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Two-way sync between rpiv-todo overlay and bd issue tracker",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/token-estimation",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — token-estimation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tooling-preference",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — JS/TS tooling preference (Biome vs ESLint/Prettier) proposal",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/trajectory",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Trajectory capture for the write-review loop — persists cycle records for learning",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/tui",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — tui",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/two-phase-review",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/types",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Shared TypeScript types for pi-harness-runtime packages",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/webmcp-tester",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "WebMCP-powered E2E testing for pi-harness - natural language test scenarios",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/wiki",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "LLM Wiki integration for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/write-review",
3
- "version": "1.1.459",
3
+ "version": "1.1.460",
4
4
  "description": "Two-agent write-review loop with blackboard coordination",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",