pi-harness-runtime 1.1.447 → 1.1.450

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 (73) hide show
  1. package/AGENTS.md +82 -9
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +74 -0
  4. package/harness/key-file-audit.ts +252 -42
  5. package/harness/startup-report.ts +32 -7
  6. package/index.ts +142 -22
  7. package/package.json +2 -1
  8. package/packages/agent-policy/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-runtime/package.json +1 -1
  11. package/packages/checkpoint/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/codex-watcher/package.json +1 -1
  14. package/packages/config-capture/package.json +1 -1
  15. package/packages/contracts/package.json +1 -1
  16. package/packages/cookie-sanitizer/package.json +1 -1
  17. package/packages/evaluation-engine/package.json +1 -1
  18. package/packages/event-bus/package.json +1 -1
  19. package/packages/event-store/package.json +1 -1
  20. package/packages/experience-replay/package.json +1 -1
  21. package/packages/file-copy-helper/package.json +1 -1
  22. package/packages/intent-analyzer/package.json +1 -1
  23. package/packages/jev-judge/dist/auto-continue.d.ts +39 -1
  24. package/packages/jev-judge/dist/auto-continue.d.ts.map +1 -1
  25. package/packages/jev-judge/dist/auto-continue.js +91 -3
  26. package/packages/jev-judge/dist/auto-continue.js.map +1 -1
  27. package/packages/jev-judge/dist/autonomy-instruction.d.ts +52 -0
  28. package/packages/jev-judge/dist/autonomy-instruction.d.ts.map +1 -0
  29. package/packages/jev-judge/dist/autonomy-instruction.js +92 -0
  30. package/packages/jev-judge/dist/autonomy-instruction.js.map +1 -0
  31. package/packages/jev-judge/dist/index.d.ts +3 -1
  32. package/packages/jev-judge/dist/index.d.ts.map +1 -1
  33. package/packages/jev-judge/dist/index.js +2 -0
  34. package/packages/jev-judge/dist/index.js.map +1 -1
  35. package/packages/jev-judge/dist/question-risk.d.ts +8 -0
  36. package/packages/jev-judge/dist/question-risk.d.ts.map +1 -1
  37. package/packages/jev-judge/dist/question-risk.js +7 -0
  38. package/packages/jev-judge/dist/question-risk.js.map +1 -1
  39. package/packages/jev-judge/package.json +1 -1
  40. package/packages/jev-judge/src/auto-continue.ts +134 -5
  41. package/packages/jev-judge/src/autonomy-instruction.ts +140 -0
  42. package/packages/jev-judge/src/index.ts +16 -1
  43. package/packages/jev-judge/src/question-risk.ts +16 -0
  44. package/packages/knowledge-retrieval/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/logging/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/notification/dist/pending-question.d.ts +114 -3
  49. package/packages/notification/dist/pending-question.d.ts.map +1 -1
  50. package/packages/notification/dist/pending-question.js +277 -12
  51. package/packages/notification/dist/pending-question.js.map +1 -1
  52. package/packages/notification/package.json +1 -1
  53. package/packages/notification/src/pending-question.ts +389 -11
  54. package/packages/paperclip-integration/package.json +1 -1
  55. package/packages/performance-optimizer/package.json +1 -1
  56. package/packages/privilege-broker/package.json +1 -1
  57. package/packages/prompt-compiler/package.json +1 -1
  58. package/packages/providers/package.json +1 -1
  59. package/packages/qdrant-skills/package.json +1 -1
  60. package/packages/quota-manager/package.json +1 -1
  61. package/packages/relay/package.json +1 -1
  62. package/packages/skill-mcp-client/package.json +1 -1
  63. package/packages/skills/package.json +1 -1
  64. package/packages/todo-bd-sync/package.json +1 -1
  65. package/packages/token-estimation/package.json +1 -1
  66. package/packages/tooling-preference/package.json +1 -1
  67. package/packages/trajectory/package.json +1 -1
  68. package/packages/tui/package.json +1 -1
  69. package/packages/two-phase-review/package.json +1 -1
  70. package/packages/types/package.json +1 -1
  71. package/packages/webmcp-tester/package.json +1 -1
  72. package/packages/wiki/package.json +1 -1
  73. package/packages/write-review/package.json +1 -1
@@ -13,9 +13,30 @@ import { logWarn } from "./file-logger.js";
13
13
  export type QuestionKind =
14
14
  | "routine" // "Should I continue?"
15
15
  | "approval" // "Want me to implement X?"
16
+ | "choice" // "Do you want X, or is Y intended?"
16
17
  | "clarification" // "Do you mean X or Y?"
17
18
  | "explicit"; // Explicit approval required
18
19
 
20
+ /**
21
+ * The typed ask of a detected question.
22
+ *
23
+ * An ask is not the same thing as a question: a question is a sentence, an
24
+ * ask is what the agent wants. The distinction matters after the timeout:
25
+ * an ask of type "choice" can be decided autonomously (the decision names
26
+ * the chosen option), while an ask of type "clarification" or
27
+ * "explicit_authorization" waits for a human.
28
+ *
29
+ * A question the detector cannot interpret is NOT a routine ask: an
30
+ * unrecognized question is a clarification, because the harness does not
31
+ * know what the agent is waiting for and must not guess.
32
+ */
33
+ export type AskType =
34
+ | "approval"
35
+ | "choice"
36
+ | "clarification"
37
+ | "explicit_authorization"
38
+ | "routine";
39
+
19
40
  /**
20
41
  * Question states
21
42
  */
@@ -69,6 +90,18 @@ export interface PendingQuestion {
69
90
  questionText: string;
70
91
  /** Question kind */
71
92
  kind: QuestionKind;
93
+ /** The typed ask of the question. */
94
+ askType?: AskType;
95
+ /** The concrete alternatives of a choice question. */
96
+ alternatives?: string[];
97
+ /** The complete assistant message the question was detected in. */
98
+ contextText?: string;
99
+ /** The option Jev selected for a choice question. */
100
+ selectedOption?: string;
101
+ /** The scope Jev chose for the selected option. */
102
+ scope?: DecisionScope;
103
+ /** The safe reason code of the last state transition. */
104
+ closedReason?: string;
72
105
  /** Does the proposed action need an explicit human authorization? */
73
106
  requiresExplicitAuthorization?: boolean;
74
107
  /** Can the proposed action destroy or damage something? */
@@ -118,6 +151,205 @@ export interface DetectionResult {
118
151
  questionText?: string;
119
152
  kind?: QuestionKind;
120
153
  isRoutine?: boolean;
154
+ /** The typed ask of the detected question. */
155
+ askType?: AskType;
156
+ /** The concrete alternatives of a choice question. */
157
+ alternatives?: string[];
158
+ /** The complete assistant message the question was detected in. */
159
+ contextText?: string;
160
+ }
161
+
162
+ /**
163
+ * The scope of an autonomous decision.
164
+ *
165
+ * A large or expensive job is not done in one step: the decision may
166
+ * limit itself to a canary that is verified before the work scales up.
167
+ */
168
+ export type DecisionScope = "full_scope" | "canary_first" | "none";
169
+
170
+ /**
171
+ * The structured resolution of an autonomous decision.
172
+ *
173
+ * It is the answer to the question the agent asked: the selected option,
174
+ * the scope and the reasoning. It is never a human answer.
175
+ */
176
+ export interface JevResolution {
177
+ /** The option Jev selected for a choice question. */
178
+ selectedOption?: string;
179
+ /** The scope Jev chose. */
180
+ scope?: DecisionScope;
181
+ /** The reasoning of the decision. */
182
+ reasoning?: string;
183
+ }
184
+
185
+ /**
186
+ * The wording that asks for an explicit human authorization.
187
+ *
188
+ * The agent states the requirement itself ("I need your OK before ..."). It
189
+ * is part of the context of the question, not of the question sentence, so
190
+ * it is detected on the whole message.
191
+ */
192
+ const EXPLICIT_AUTHORIZATION_PATTERNS: RegExp[] = [
193
+ /\b(?:need|needs|needing|require|requires|requiring|want|wants|waiting for|wait for)\s+(?:your|the)\s+(?:explicit\s+)?(?:ok|okay|approval|approbation|authorization|authorisation|permission|consent|confirmation|green light|sign[- ]?off|go[- ]?ahead)\b/i,
194
+ /\bexplicit\s+(?:approval|authorization|authorisation|permission)\b/i,
195
+ /\b(?:authorize|authorise)\s+me\b/i,
196
+ /\bonly\s+(?:with|after)\s+your\b/i,
197
+ ];
198
+
199
+ /**
200
+ * Does the message ask for an explicit human authorization?
201
+ *
202
+ * An ask of this kind is never decided autonomously: only the human may
203
+ * authorize it.
204
+ */
205
+ export function hasExplicitAuthorizationWording(text: string): boolean {
206
+ return EXPLICIT_AUTHORIZATION_PATTERNS.some((pattern) => pattern.test(text));
207
+ }
208
+
209
+ /**
210
+ * The typed ask of a question kind.
211
+ */
212
+ export function askTypeForKind(kind: QuestionKind): AskType {
213
+ switch (kind) {
214
+ case "approval":
215
+ return "approval";
216
+ case "choice":
217
+ return "choice";
218
+ case "clarification":
219
+ return "clarification";
220
+ case "explicit":
221
+ return "explicit_authorization";
222
+ case "routine":
223
+ return "routine";
224
+ default:
225
+ // An unknown kind is a clarification: the harness does not know
226
+ // what the agent is waiting for, so it waits for a human.
227
+ return "clarification";
228
+ }
229
+ }
230
+
231
+ /**
232
+ * The typed ask of a detected question.
233
+ *
234
+ * A choice keeps its type: the agent offers concrete alternatives, and a
235
+ * decision may pick one of them. Every other ask that the message
236
+ * explicitly reserves for the human is an explicit authorization.
237
+ */
238
+ export function computeAskType(
239
+ kind: QuestionKind,
240
+ explicitAuthorization: boolean,
241
+ ): AskType {
242
+ if (kind === "choice") return "choice";
243
+ if (explicitAuthorization) return "explicit_authorization";
244
+ return askTypeForKind(kind);
245
+ }
246
+
247
+ /**
248
+ * The stem of a question: the part that carries no alternative.
249
+ */
250
+ const QUESTION_STEM =
251
+ /^(?:do you want(?: me to)?|do you prefer|would you like me to|should i|should we|shall i|shall we|may i|can i|could i|is it|are you|which|what|do)\s+/i;
252
+
253
+ /** The auxiliary verb of the second alternative. */
254
+ const ALTERNATIVE_AUXILIARY =
255
+ /^(?:is|are|was|were|should|would|do|does|did|can|could|shall|will|to)\s+/i;
256
+
257
+ /** The intent tail of an alternative ("... is intended"). */
258
+ const ALTERNATIVE_INTENT =
259
+ /\s+(intended|expected|needed|required|desired|meant|correct|right|preferred)$/i;
260
+
261
+ /**
262
+ * The longest question text that is parsed.
263
+ *
264
+ * A question is a sentence. The cap keeps the work of the parser
265
+ * bounded: the alternatives of a question are not longer than a line,
266
+ * and a bounded input cannot make a slow pattern slow.
267
+ */
268
+ const MAX_QUESTION_LENGTH = 500;
269
+
270
+ /** Is the character whitespace? */
271
+ function isSpace(character: string | undefined): boolean {
272
+ return typeof character === "string" && /\s/.test(character);
273
+ }
274
+
275
+ /** Is the character the end of a sentence? */
276
+ function isSentenceEnd(character: string | undefined): boolean {
277
+ return character === "?" || character === "!" || character === ".";
278
+ }
279
+
280
+ /**
281
+ * The question without the punctuation that ends the sentence.
282
+ *
283
+ * The punctuation is removed by a scan and not by a pattern: a pattern
284
+ * with two unbounded repetitions next to each other takes quadratic time
285
+ * on a long run of the same character.
286
+ */
287
+ function withoutSentenceEnd(text: string): string {
288
+ let end = text.length;
289
+ while (end > 0 && isSentenceEnd(text[end - 1])) end -= 1;
290
+ return text.slice(0, end).trimEnd();
291
+ }
292
+
293
+ /**
294
+ * The index where the "or" of two alternatives starts.
295
+ *
296
+ * The separator is the word "or" with whitespace around it. A comma in
297
+ * front of it belongs to the separator. The search is a scan over the
298
+ * string: a pattern with two unbounded repetitions can take quadratic
299
+ * time, and a question is not a fixed length.
300
+ *
301
+ * @returns the boundaries of the separator, or null when there is none.
302
+ */
303
+ function findOrSeparator(text: string): { leftEnd: number; orIndex: number } | null {
304
+ const lower = text.toLowerCase();
305
+ for (let index = 0; index + 1 < text.length; index += 1) {
306
+ if (lower[index] !== "o" || lower[index + 1] !== "r") continue;
307
+ if (!isSpace(text[index - 1]) || !isSpace(text[index + 2])) continue;
308
+ // The whitespace in front of the word, and the comma that may
309
+ // come with it.
310
+ let start = index;
311
+ while (start > 0 && isSpace(text[start - 1])) start -= 1;
312
+ const leftEnd = text[start - 1] === "," ? start - 1 : start;
313
+ return { leftEnd, orIndex: index };
314
+ }
315
+ return null;
316
+ }
317
+
318
+ /**
319
+ * Extract the alternatives of a choice question.
320
+ *
321
+ * The result is what the decision has to choose between, so it must be
322
+ * concrete: "the current staging state is intended" is an alternative,
323
+ * "is the current staging state intended?" is not. An empty or unusable
324
+ * half means the question is not a choice after all.
325
+ */
326
+ export function extractAlternatives(question: string): string[] {
327
+ const text = withoutSentenceEnd(
328
+ question.trim().slice(0, MAX_QUESTION_LENGTH),
329
+ );
330
+
331
+ const separator = findOrSeparator(text);
332
+ if (!separator) return [];
333
+
334
+ // The left half ends before the separator, the right half after the
335
+ // word "or".
336
+ const left = text
337
+ .slice(0, separator.leftEnd)
338
+ .replace(QUESTION_STEM, "")
339
+ .replace(/[\s,]+$/, "")
340
+ .trim();
341
+ const rightRaw = text.slice(separator.orIndex + 2).trim();
342
+ const right = rightRaw.replace(ALTERNATIVE_AUXILIARY, "").trim();
343
+
344
+ if (!left || !right) return [];
345
+
346
+ // "the current staging state intended" -> "the current staging state is intended"
347
+ const intent = right.match(ALTERNATIVE_INTENT);
348
+ const normalizedRight = intent
349
+ ? `${right.slice(0, intent.index).trimEnd()} is ${intent[1]}`
350
+ : right;
351
+
352
+ return [left, normalizedRight];
121
353
  }
122
354
 
123
355
  /**
@@ -176,13 +408,26 @@ export class PendingQuestionController {
176
408
 
177
409
  /**
178
410
  * Detect if text contains a question requiring response
411
+ *
412
+ * The complete assistant message is kept as the context of the question:
413
+ * the sentence that carries the question often leaves out what the agent
414
+ * is really waiting for ("I need your OK before ..."). The risk facts and
415
+ * the decision are derived from the whole message, not from the sentence
416
+ * alone.
179
417
  */
180
- detectQuestion(text: string, _options?: {
418
+ detectQuestion(text: string, options?: {
181
419
  sessionId?: string;
182
420
  workspacePath?: string;
183
421
  jobId?: string;
422
+ /** The complete assistant message the question was detected in. */
423
+ contextText?: string;
184
424
  }
185
425
  ): DetectionResult {
426
+ const contextText = options?.contextText ?? text;
427
+ // The message decides whether the agent reserves the decision for the
428
+ // human: "I need your OK before ..." is part of the context and not
429
+ // of the question sentence.
430
+ const explicitAuthorization = hasExplicitAuthorizationWording(contextText);
186
431
  // Find all question positions and extract each question sentence
187
432
  const questionPositions: Array<{ start: number; end: number; sentence: string }> = [];
188
433
  let searchFrom = 0;
@@ -231,30 +476,39 @@ export class PendingQuestionController {
231
476
  const isRoutine = this.isRoutineContinuation(q.sentence);
232
477
  const kind = this.determineKind(q.sentence, isRoutine);
233
478
 
234
- // Return first approval or clarification question found
235
- if (kind === "approval" || kind === "clarification") {
479
+ // Return first approval, choice or clarification question found
480
+ if (kind === "approval" || kind === "choice" || kind === "clarification") {
236
481
  return {
237
482
  detected: true,
238
483
  questionText: q.sentence,
239
484
  kind,
240
485
  isRoutine,
486
+ askType: computeAskType(kind, explicitAuthorization),
487
+ alternatives: kind === "choice" ? extractAlternatives(q.sentence) : undefined,
488
+ contextText,
241
489
  };
242
490
  }
243
491
  }
244
492
 
245
- // If no approval/clarification found, return first non-quoted routine question
493
+ // If no approval/choice/clarification was found, return the first
494
+ // non-quoted question. An unrecognized question is a clarification:
495
+ // the harness does not know what the agent is waiting for.
246
496
  for (const q of questionPositions) {
247
497
  if (this.isQuotedOrCode(text, q.sentence)) {
248
498
  continue;
249
499
  }
250
500
 
251
501
  const isRoutine = this.isRoutineContinuation(q.sentence);
502
+ const kind = this.determineKind(q.sentence, isRoutine);
252
503
 
253
504
  return {
254
505
  detected: true,
255
506
  questionText: q.sentence,
256
- kind: "routine",
507
+ kind,
257
508
  isRoutine,
509
+ askType: computeAskType(kind, explicitAuthorization),
510
+ alternatives: kind === "choice" ? extractAlternatives(q.sentence) : undefined,
511
+ contextText,
258
512
  };
259
513
  }
260
514
 
@@ -305,6 +559,11 @@ export class PendingQuestionController {
305
559
  questionText: text.trim(),
306
560
  kind: "approval",
307
561
  isRoutine: false,
562
+ askType: computeAskType(
563
+ "approval",
564
+ hasExplicitAuthorizationWording(text),
565
+ ),
566
+ contextText: text,
308
567
  };
309
568
  }
310
569
  }
@@ -386,10 +645,8 @@ export class PendingQuestionController {
386
645
  /\bwant me to\b/i,
387
646
  /\bwould you like me to\b/i,
388
647
  /\bshall i\b/i,
389
- /\bshould i add\b/i,
390
- /\bshould i implement\b/i,
391
- /\bshould we add\b/i,
392
- /\bshould we implement\b/i,
648
+ /\bshould i\b/i,
649
+ /\bshould we\b/i,
393
650
  /\bdo you want me to\b/i,
394
651
  /\bcan i (start|proceed|add|implement)\b/i,
395
652
  ];
@@ -400,7 +657,25 @@ export class PendingQuestionController {
400
657
  }
401
658
  }
402
659
 
403
- return "routine";
660
+ // A choice names concrete alternatives: "do you want X, or is Y
661
+ // intended?". The decision may pick one of them, but only when it
662
+ // names the picked option.
663
+ const choicePatterns = [
664
+ /,\s*or\s+(?:is|are|was|were|should|would|do|does|did|can|could|shall|will|to)\b/i,
665
+ /\beither\b[^?]*\bor\b/i,
666
+ /\b(?:do you want|do you prefer|would you like|should i|shall i|can i|could i|which)\b[^?]*\bor\b/i,
667
+ ];
668
+
669
+ for (const pattern of choicePatterns) {
670
+ if (pattern.test(lower)) {
671
+ return "choice";
672
+ }
673
+ }
674
+
675
+ // An unrecognized question is not a routine continuation: the harness
676
+ // does not know what the agent is waiting for, so it asks a human
677
+ // instead of continuing on a guess.
678
+ return "clarification";
404
679
  }
405
680
 
406
681
  /**
@@ -447,6 +722,12 @@ export class PendingQuestionController {
447
722
  workspacePath: string;
448
723
  jobId?: string;
449
724
  kind: QuestionKind;
725
+ /** The typed ask of the question. */
726
+ askType?: AskType;
727
+ /** The concrete alternatives of a choice question. */
728
+ alternatives?: string[];
729
+ /** The complete assistant message the question was detected in. */
730
+ contextText?: string;
450
731
  /** The deterministic risk facts of the proposed action. */
451
732
  risk?: {
452
733
  requiresExplicitAuthorization?: boolean;
@@ -465,6 +746,9 @@ export class PendingQuestionController {
465
746
  jobId: params.jobId,
466
747
  questionText: params.text,
467
748
  kind: params.kind,
749
+ askType: params.askType ?? askTypeForKind(params.kind),
750
+ alternatives: params.alternatives,
751
+ contextText: params.contextText,
468
752
  requiresExplicitAuthorization: params.risk?.requiresExplicitAuthorization,
469
753
  destructive: params.risk?.destructive,
470
754
  reversible: params.risk?.reversible,
@@ -554,6 +838,18 @@ export class PendingQuestionController {
554
838
  return false;
555
839
  }
556
840
 
841
+ // A question that was expired or cancelled is closed: a late click
842
+ // must not resurrect it and must not report an answer that never
843
+ // happened.
844
+ if (
845
+ this.currentQuestion.state === "expired" ||
846
+ this.currentQuestion.state === "cancelled"
847
+ ) {
848
+ logWarn("[PendingQuestion]",
849
+ `Question is ${this.currentQuestion.state} (${this.currentQuestion.closedReason ?? "no reason"}) - late callback ignored`);
850
+ return false;
851
+ }
852
+
557
853
  this.currentQuestion.state = "resolved";
558
854
  this.currentQuestion.resolution = params.resolution;
559
855
  this.currentQuestion.resolvedBy = params.by;
@@ -595,6 +891,7 @@ export class PendingQuestionController {
595
891
  requestId: string,
596
892
  outcome: JevOutcome,
597
893
  reason?: string,
894
+ resolution?: JevResolution,
598
895
  ): boolean {
599
896
  if (!this.currentQuestion) {
600
897
  logWarn("[PendingQuestion]", "No question to resolve - no pending question");
@@ -613,27 +910,108 @@ export class PendingQuestionController {
613
910
  return false;
614
911
  }
615
912
 
913
+ if (
914
+ this.currentQuestion.state === "expired" ||
915
+ this.currentQuestion.state === "cancelled"
916
+ ) {
917
+ logWarn("[PendingQuestion]",
918
+ `Question is ${this.currentQuestion.state} - jev decision ignored`);
919
+ return false;
920
+ }
921
+
616
922
  this.currentQuestion.state = "resolved";
617
923
  this.currentQuestion.resolvedBy = "jev";
618
924
  this.currentQuestion.jevOutcome = outcome;
619
925
  this.currentQuestion.jevReason = reason;
620
926
  this.currentQuestion.resolvedAt = new Date();
927
+ // The resolution is the answer to the question: the selected option and
928
+ // the scope travel with the request, so the agent can be told which
929
+ // option was chosen.
930
+ if (resolution?.selectedOption) {
931
+ this.currentQuestion.selectedOption = resolution.selectedOption;
932
+ }
933
+ if (resolution?.scope) {
934
+ this.currentQuestion.scope = resolution.scope;
935
+ }
621
936
 
622
937
  logWarn("[PendingQuestion]",
623
938
  `Closed question ${this.currentQuestion.requestId} with jev outcome ${outcome}`);
624
939
 
625
940
  if (this.jevDecisionCallback) {
941
+ const metadata: Record<string, unknown> = { outcome };
942
+ if (resolution?.selectedOption) {
943
+ metadata["selectedOption"] = resolution.selectedOption;
944
+ }
945
+ if (resolution?.scope) {
946
+ metadata["scope"] = resolution.scope;
947
+ }
626
948
  this.jevDecisionCallback(this.currentQuestion, {
627
949
  action: outcome === "reject" ? "stop" : outcome === "wait" ? "wait" : "continue",
628
950
  reason: reason ?? `jev:${outcome}`,
629
951
  eligible: true,
630
- metadata: { outcome },
952
+ metadata,
953
+ });
954
+ }
955
+
956
+ return true;
957
+ }
958
+
959
+ /**
960
+ * Expire the pending question when the decision deadline is reached.
961
+ *
962
+ * The deadline is a terminal path: the question is closed with the safe
963
+ * reason code, the buttons of that request become stale, and the wait
964
+ * state is cleared. A question that is left open here looks like a
965
+ * request that is still waiting - forever.
966
+ *
967
+ * @returns true if the question was expired, false when it was already
968
+ * answered by a human or closed by a decision.
969
+ */
970
+ expireByTimeout(requestId: string, reason: string = "deadline-expired"): boolean {
971
+ if (!this.currentQuestion) {
972
+ logWarn("[PendingQuestion]", "No question to expire - no pending question");
973
+ return false;
974
+ }
975
+
976
+ if (this.currentQuestion.requestId !== requestId) {
977
+ logWarn("[PendingQuestion]",
978
+ `Expiry for an unknown request: ${requestId} (current=${this.currentQuestion.requestId})`);
979
+ return false;
980
+ }
981
+
982
+ if (this.currentQuestion.state !== "pending" && this.currentQuestion.state !== "evaluating") {
983
+ logWarn("[PendingQuestion]",
984
+ `Question is ${this.currentQuestion.state} - expiry ignored`);
985
+ return false;
986
+ }
987
+
988
+ this.currentQuestion.state = "expired";
989
+ this.currentQuestion.resolvedBy = "timeout";
990
+ this.currentQuestion.resolvedAt = new Date();
991
+ this.currentQuestion.closedReason = reason;
992
+
993
+ logWarn("[PendingQuestion]",
994
+ `Expired question ${this.currentQuestion.requestId}: ${reason}`);
995
+
996
+ if (this.jevDecisionCallback) {
997
+ this.jevDecisionCallback(this.currentQuestion, {
998
+ action: "stop",
999
+ reason,
1000
+ eligible: false,
1001
+ metadata: { outcome: "expired" },
631
1002
  });
632
1003
  }
633
1004
 
634
1005
  return true;
635
1006
  }
636
1007
 
1008
+ /**
1009
+ * The state of the current question.
1010
+ */
1011
+ getState(): QuestionState | undefined {
1012
+ return this.currentQuestion?.state;
1013
+ }
1014
+
637
1015
  /**
638
1016
  * Apply an approval action from a channel to the current question.
639
1017
  *
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/paperclip-integration",
3
- "version": "1.1.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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.447",
3
+ "version": "1.1.450",
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",