pi-harness-runtime 1.1.38 → 1.1.39

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 (94) hide show
  1. package/harness/langchain/daemon.ts +74 -1
  2. package/harness/langchain/graph.ts +3 -0
  3. package/harness/langchain/widget.test.ts +244 -0
  4. package/harness/langchain/widget.ts +463 -0
  5. package/package.json +1 -1
  6. package/packages/a2a-adapter/package.json +1 -1
  7. package/packages/architecture-generator/package.json +1 -1
  8. package/packages/auth/package.json +1 -1
  9. package/packages/autonomous-refactor/package.json +1 -1
  10. package/packages/autonomous-runtime/package.json +1 -1
  11. package/packages/cache-strategy/package.json +1 -1
  12. package/packages/capability-registry/package.json +1 -1
  13. package/packages/checkpoint/package.json +1 -1
  14. package/packages/cli-plugin-sdk/package.json +1 -1
  15. package/packages/clipboard/package.json +1 -1
  16. package/packages/clipboard-plugin/package.json +1 -1
  17. package/packages/code-generation/package.json +1 -1
  18. package/packages/code-review/package.json +1 -1
  19. package/packages/codex-adapter/package.json +1 -1
  20. package/packages/config-capture/package.json +1 -1
  21. package/packages/context-compiler/package.json +1 -1
  22. package/packages/context-discovery/package.json +1 -1
  23. package/packages/context-manager/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/evaluation-engine/package.json +1 -1
  30. package/packages/evaluation-runner/package.json +1 -1
  31. package/packages/event-bus/package.json +1 -1
  32. package/packages/event-store/package.json +1 -1
  33. package/packages/experience-replay/package.json +1 -1
  34. package/packages/feedback-collector/package.json +1 -1
  35. package/packages/file-copy-helper/package.json +1 -1
  36. package/packages/framework-detector/package.json +1 -1
  37. package/packages/framework-plugin-sdk/package.json +1 -1
  38. package/packages/frappe-plugin/package.json +1 -1
  39. package/packages/generic-web-plugin/package.json +1 -1
  40. package/packages/health-monitor/package.json +1 -1
  41. package/packages/intent-analyzer/package.json +1 -1
  42. package/packages/knowledge-graph/package.json +1 -1
  43. package/packages/knowledge-retrieval/package.json +1 -1
  44. package/packages/laravel-plugin/package.json +1 -1
  45. package/packages/learning-engine/package.json +1 -1
  46. package/packages/mcp-adapter/package.json +1 -1
  47. package/packages/memory-engine/package.json +1 -1
  48. package/packages/milestone-manager/package.json +1 -1
  49. package/packages/model-registry/package.json +1 -1
  50. package/packages/nextjs-plugin/package.json +1 -1
  51. package/packages/notification/package.json +1 -1
  52. package/packages/observability/package.json +1 -1
  53. package/packages/okf-indexer/package.json +1 -1
  54. package/packages/performance-optimizer/package.json +1 -1
  55. package/packages/privilege-broker/package.json +1 -1
  56. package/packages/project-analyzer/package.json +1 -1
  57. package/packages/project-bootstrap/package.json +1 -1
  58. package/packages/projection-engine/package.json +1 -1
  59. package/packages/prompt-compiler/package.json +1 -1
  60. package/packages/prompt-versioning/package.json +1 -1
  61. package/packages/provider-adapter-sdk/package.json +1 -1
  62. package/packages/provider-router/package.json +1 -1
  63. package/packages/provider-selector/package.json +1 -1
  64. package/packages/providers/package.json +1 -1
  65. package/packages/quota-manager/package.json +1 -1
  66. package/packages/rate-limiter/package.json +1 -1
  67. package/packages/react-vite-plugin/package.json +1 -1
  68. package/packages/release-manager/package.json +1 -1
  69. package/packages/requirement-compiler/package.json +1 -1
  70. package/packages/runtime/package.json +1 -1
  71. package/packages/scheduler/package.json +1 -1
  72. package/packages/scheduler-adapter/package.json +1 -1
  73. package/packages/session/package.json +1 -1
  74. package/packages/session-api/package.json +1 -1
  75. package/packages/session-export/package.json +1 -1
  76. package/packages/shared-context/package.json +1 -1
  77. package/packages/skill-mcp-client/package.json +1 -1
  78. package/packages/skill-registry/package.json +1 -1
  79. package/packages/sprint-planner/package.json +1 -1
  80. package/packages/subscription-engine/package.json +1 -1
  81. package/packages/task-compiler/package.json +1 -1
  82. package/packages/tencentdb-memory/package.json +1 -1
  83. package/packages/tencentdb-sync/package.json +1 -1
  84. package/packages/test-data-generator/package.json +1 -1
  85. package/packages/test-generator/package.json +1 -1
  86. package/packages/todo-bd-sync/package.json +1 -1
  87. package/packages/token-estimation/package.json +1 -1
  88. package/packages/token-optimizer/package.json +1 -1
  89. package/packages/tui/package.json +1 -1
  90. package/packages/types/package.json +1 -1
  91. package/packages/workflow-events/package.json +1 -1
  92. package/packages/workspace-scanner/package.json +1 -1
  93. package/packages/worktree/package.json +1 -1
  94. package/packages/write-review/package.json +1 -1
@@ -48,6 +48,7 @@ import {
48
48
  import { NotificationCenter } from "../../packages/notification/dist/notification-center.js";
49
49
 
50
50
  import { invokeWithSurgeRetry, type SurgePolicy } from "./surge.js";
51
+ import type { LoopWidget } from "./widget.js";
51
52
  import { createLoopCheckpointer } from "./checkpointer.js";
52
53
 
53
54
  import {
@@ -111,6 +112,12 @@ export interface DaemonConfig {
111
112
  * Default: none (no tunnel management)
112
113
  */
113
114
  tunnelCommand?: string;
115
+ /**
116
+ * Optional widget for TUI / status-line display.
117
+ * When set, the daemon updates the widget at each loop transition
118
+ * (mirrors pi-lens footer style: !NW ●ME).
119
+ */
120
+ widget?: LoopWidget;
114
121
  }
115
122
 
116
123
  // ─── TriggeredTask ──────────────────────────────────────────────────────────
@@ -668,6 +675,7 @@ export class LoopDaemon {
668
675
  private readonly busWatcher: BusWatcher;
669
676
  private readonly bdWatcher: BdTasksWatcher;
670
677
  private readonly tunnelMonitor: TunnelMonitor | null;
678
+ private readonly widget: LoopWidget | null;
671
679
 
672
680
  /** Currently running tasks (taskId → RunningTask) */
673
681
  private readonly running = new Map<string, RunningTask>();
@@ -691,6 +699,7 @@ export class LoopDaemon {
691
699
  workspace: config.workspace ?? getHerdrWorkspace(),
692
700
  notificationConfig: config.notificationConfig ?? undefined,
693
701
  tunnelCommand: config.tunnelCommand,
702
+ widget: config.widget,
694
703
  } as Required<DaemonConfig>;
695
704
 
696
705
  this.agentId = this.config.agentId;
@@ -718,6 +727,7 @@ export class LoopDaemon {
718
727
  }
719
728
 
720
729
  this.inboxWatcher = new InboxWatcher(this.inboxDir, this.config.pollMs);
730
+ this.widget = this.config.widget ?? null;
721
731
  this.busWatcher = new BusWatcher(this.bus, this.config.pollMs);
722
732
  this.bdWatcher = new BdTasksWatcher(this.config.pollMs);
723
733
 
@@ -879,11 +889,12 @@ export class LoopDaemon {
879
889
  maxIterations: this.config.maxIterations,
880
890
  }));
881
891
 
882
- // Inject transition publishing into onStep
892
+ // Inject transition publishing into onStep (also drives the widget)
883
893
  const originalOnStep = deps.onStep;
884
894
  deps.onStep = (step, state) => {
885
895
  originalOnStep?.(step, state);
886
896
  this._publishTransition(loopId, step, state);
897
+ this._updateWidget(step, state);
887
898
  };
888
899
 
889
900
  // ── Run the graph (surge-aware: 529/overloaded pauses auto-resume) ──
@@ -952,6 +963,17 @@ export class LoopDaemon {
952
963
 
953
964
  log(`Loop finished: ${verdict} after ${iterations} iteration(s)`);
954
965
 
966
+ // Update widget on loop completion
967
+ if (this.widget) {
968
+ this.widget.setComplete(
969
+ verdict === "approved"
970
+ ? "approved"
971
+ : verdict === "blocked"
972
+ ? "rejected"
973
+ : "max_iterations",
974
+ );
975
+ }
976
+
955
977
  // ── Gate check after blocked verdict ────────────────────────────────
956
978
  if (
957
979
  verdict === "blocked" &&
@@ -1034,6 +1056,57 @@ export class LoopDaemon {
1034
1056
  }
1035
1057
  }
1036
1058
 
1059
+ /**
1060
+ * Drive the TUI widget through loop transitions.
1061
+ * Called from onStep within _runTask.
1062
+ */
1063
+ private _updateWidget(step: string, state: LoopState): void {
1064
+ const w = this.widget;
1065
+ if (!w) return;
1066
+
1067
+ switch (step) {
1068
+ case "plan": {
1069
+ w.setPhase("planning", "GPT planning");
1070
+ break;
1071
+ }
1072
+ case "write": {
1073
+ w.setPhase("writing", "MiniMax coding");
1074
+ // After a write step, bump the iteration counter
1075
+ w.startIteration(state.iteration, this.config.maxIterations);
1076
+ break;
1077
+ }
1078
+ case "review": {
1079
+ w.setPhase("reviewing", "GPT reviewing");
1080
+ // Process review comments — one record per file
1081
+ const comments = state.review?.comments ?? [];
1082
+ if (comments.length === 0) break;
1083
+
1084
+ // Group blocking comments by file (severity: "critical" = blocking)
1085
+ const byFile = new Map<string, number>();
1086
+ for (const c of comments) {
1087
+ if (c.severity === "critical") {
1088
+ const prev = byFile.get(c.file ?? "") ?? 0;
1089
+ byFile.set(c.file ?? "", prev + 1);
1090
+ }
1091
+ }
1092
+ for (const [file, count] of byFile) {
1093
+ if (count > 0) {
1094
+ w.recordBlockers(file, count);
1095
+ }
1096
+ }
1097
+ // Files with no blocking comments pass
1098
+ const blockingFiles = new Set(byFile.keys());
1099
+ for (const c of comments) {
1100
+ const f = c.file ?? "";
1101
+ if (!blockingFiles.has(f) && f) {
1102
+ w.recordReviewPass(f);
1103
+ }
1104
+ }
1105
+ break;
1106
+ }
1107
+ }
1108
+ }
1109
+
1037
1110
  private _reviewReportPath(loopId: string, iteration: number): string {
1038
1111
  return `${ensureHerdrWorkspace().reviews}/${loopId}-review-${iteration}.md`;
1039
1112
  }
@@ -24,6 +24,7 @@ import {
24
24
  type ReviewVerdict,
25
25
  ReviewVerdictSchema,
26
26
  } from "./agents.js";
27
+ import type { LoopWidget } from "./widget.js";
27
28
 
28
29
  // ─── State ──────────────────────────────────────────────────────────────────
29
30
 
@@ -58,6 +59,8 @@ export interface LoopDeps {
58
59
  review: (plan: string, code: string) => Promise<ReviewVerdict>;
59
60
  maxIterations: number;
60
61
  onStep?: (step: string, state: LoopState) => void;
62
+ /** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
63
+ widget?: LoopWidget;
61
64
  }
62
65
 
63
66
  // ─── Node implementations ───────────────────────────────────────────────────
@@ -0,0 +1,244 @@
1
+ /**
2
+ * LoopWidget tests — T14
3
+ *
4
+ * Run:
5
+ * bun test harness/langchain/widget.test.ts
6
+ */
7
+
8
+ import { describe, it, beforeEach } from "node:test";
9
+ import assert from "node:assert";
10
+ import { LoopWidget } from "./widget.js";
11
+
12
+ // ─── Fixtures ────────────────────────────────────────────────────────────────
13
+
14
+ const stubTheme = {
15
+ fg: (color: string, s: string): string => {
16
+ const codes: Record<string, string> = {
17
+ dim: `\x1b[2m${s}\x1b[0m`,
18
+ error: `\x1b[31m${s}\x1b[0m`,
19
+ warning: `\x1b[33m${s}\x1b[0m`,
20
+ success: `\x1b[32m${s}\x1b[0m`,
21
+ accent: `\x1b[36m${s}\x1b[0m`,
22
+ };
23
+ return codes[color] ?? s;
24
+ },
25
+ };
26
+
27
+ describe("T14 — LoopWidget", () => {
28
+
29
+ let widget: LoopWidget;
30
+
31
+ beforeEach(() => {
32
+ widget = new LoopWidget();
33
+ });
34
+
35
+ // ─── Idle state ────────────────────────────────────────────────────────
36
+
37
+ it("starts in idle phase", () => {
38
+ const lines = widget.renderWidget(80, stubTheme);
39
+ assert.match(lines[0], /idle/);
40
+ assert.match(lines[0], /harness/);
41
+ });
42
+
43
+ // ─── Phase transitions ──────────────────────────────────────────────────
44
+
45
+ it("setPhase changes the phase label", () => {
46
+ widget.setPhase("writing");
47
+ const lines = widget.renderWidget(80, stubTheme);
48
+ assert.match(lines[0], /coding/);
49
+ });
50
+
51
+ it("setPhase with custom label uses the label", () => {
52
+ widget.setPhase("planning", "GPT planning");
53
+ const lines = widget.renderWidget(80, stubTheme);
54
+ assert.match(lines[0], /GPT planning/);
55
+ });
56
+
57
+ it("surge-pause shows detail line", () => {
58
+ widget.setSurgePause(new Date(Date.now() + 30 * 60_000)); // 30 min
59
+ const lines = widget.renderWidget(80, stubTheme);
60
+ // Label is "surge (in 30m)"
61
+ assert.ok(lines.some(l => l.includes("surge") && l.includes("in 30m")));
62
+ });
63
+
64
+ it("setError shows error detail", () => {
65
+ widget.setError("model timeout");
66
+ const lines = widget.renderWidget(80, stubTheme);
67
+ assert.ok(lines.some(l => l.includes("error")));
68
+ assert.ok(lines.some(l => l.includes("model timeout")));
69
+ });
70
+
71
+ it("setComplete approved shows ✓ loop complete", () => {
72
+ widget.setComplete("approved");
73
+ const lines = widget.renderWidget(80, stubTheme);
74
+ assert.match(lines[0], /loop complete/);
75
+ });
76
+
77
+ it("setComplete rejected shows ✗ loop rejected", () => {
78
+ widget.setComplete("rejected");
79
+ const lines = widget.renderWidget(80, stubTheme);
80
+ assert.match(lines[0], /loop rejected/);
81
+ });
82
+
83
+ it("setComplete max_iterations shows iteration count", () => {
84
+ widget.setComplete("max_iterations");
85
+ const lines = widget.renderWidget(80, stubTheme);
86
+ assert.match(lines[0], /max iterations/);
87
+ });
88
+
89
+ // ─── Iteration tracking ────────────────────────────────────────────────
90
+
91
+ it("startIteration sets iteration and max", () => {
92
+ widget.startIteration(3, 5);
93
+ const lines = widget.renderWidget(80, stubTheme);
94
+ assert.match(lines[0], /iter 3\/5/);
95
+ });
96
+
97
+ it("startIteration resets per-file counters but keeps file records", () => {
98
+ widget.recordWrite("src/a.ts");
99
+ widget.recordBlockers("src/a.ts", 2);
100
+ widget.startIteration(2, 5);
101
+ // Files accumulate across iterations (same codebase); only counters reset.
102
+ // After reset, a.ts shows with 0 write steps and 0 blockers.
103
+ const lines = widget.renderWidget(80, stubTheme);
104
+ const fileLines = lines.filter(l => l.includes("a.ts"));
105
+ assert.ok(fileLines.length > 0, "file should still appear");
106
+ // No blocker count in the file row (reset to 0)
107
+ assert.ok(!fileLines.some(l => l.includes("!2")), "blocker count should be reset");
108
+ });
109
+
110
+ // ─── File record tracking ──────────────────────────────────────────────
111
+
112
+ it("recordWrite shows file in widget", () => {
113
+ widget.recordWrite("src/foo.ts");
114
+ const lines = widget.renderWidget(80, stubTheme);
115
+ assert.ok(lines.some(l => l.includes("foo.ts")));
116
+ });
117
+
118
+ it("recordWrite accumulates across calls", () => {
119
+ widget.recordWrite("src/bar.ts");
120
+ widget.recordWrite("src/bar.ts");
121
+ const lines = widget.renderWidget(80, stubTheme);
122
+ assert.ok(lines.some(l => l.includes("bar.ts") && l.includes("✍2")));
123
+ });
124
+
125
+ it("recordBlockers shows blocker count", () => {
126
+ widget.recordBlockers("src/bad.ts", 2);
127
+ const lines = widget.renderWidget(80, stubTheme);
128
+ assert.ok(lines.some(l => l.includes("bad.ts") && l.includes("!2")));
129
+ });
130
+
131
+ it("recordReviewPass clears blockers and increments passes", () => {
132
+ widget.recordBlockers("src/ok.ts", 1);
133
+ widget.recordReviewPass("src/ok.ts");
134
+ const lines = widget.renderWidget(80, stubTheme);
135
+ assert.ok(lines.some(l => l.includes("ok.ts") && l.includes("🔍1")));
136
+ // No blocker count
137
+ assert.ok(!lines.some(l => l.includes("ok.ts") && l.includes("!") && !l.includes("🔍")));
138
+ });
139
+
140
+ // ─── Summary counts ────────────────────────────────────────────────────
141
+
142
+ it("no errors/warnings shows ✓", () => {
143
+ const lines = widget.renderWidget(80, stubTheme);
144
+ assert.ok(lines[0].includes("✓") || lines[0].includes("harness"));
145
+ });
146
+
147
+ it("errors shown as ●NE", () => {
148
+ widget.recordBlockers("src/err.ts", 3);
149
+ const lines = widget.renderWidget(80, stubTheme);
150
+ assert.ok(lines.some(l => l.includes("●3E")));
151
+ });
152
+
153
+ // ─── Surge pause ──────────────────────────────────────────────────────
154
+
155
+ it("setSurgePause shows reset time in minutes/hours", () => {
156
+ const resetAt = new Date(Date.now() + 90 * 60_000); // 90 minutes → "in 1h 30m"
157
+ widget.setSurgePause(resetAt);
158
+ const lines = widget.renderWidget(80, stubTheme);
159
+ const surgeLine = lines.find(l => l.includes("surge"));
160
+ assert.ok(surgeLine !== undefined);
161
+ assert.match(surgeLine, /\(in \d+h/); // "in 1h 30m" matches
162
+ });
163
+
164
+ it("setSurgePause shows 'now' for past dates", () => {
165
+ const resetAt = new Date(Date.now() - 1000);
166
+ widget.setSurgePause(resetAt);
167
+ const lines = widget.renderWidget(80, stubTheme);
168
+ // Label format: "surge (now)"
169
+ assert.ok(lines.some(l => l.includes("surge") && l.includes("(now)")));
170
+ });
171
+
172
+ // ─── Console fallback ─────────────────────────────────────────────────
173
+
174
+ it("toConsoleString returns human-readable output", () => {
175
+ widget.startIteration(2, 5);
176
+ widget.setPhase("writing", "coding");
177
+ widget.recordWrite("src/test.ts");
178
+ const output = widget.toConsoleString();
179
+ assert.match(output, /harness/);
180
+ assert.match(output, /iter 2\/5/);
181
+ assert.match(output, /coding/);
182
+ assert.match(output, /test\.ts/);
183
+ });
184
+
185
+ // ─── Width / truncation ───────────────────────────────────────────────
186
+
187
+ it("truncates long file names with …", () => {
188
+ widget.recordWrite("/very/long/path/to/a/very/long/file/name/that/exceeds/width.ts");
189
+ const lines = widget.renderWidget(40, stubTheme);
190
+ for (const line of lines) {
191
+ const stripped = line.replace(/\x1b\[[0-9;]*m/g, "");
192
+ assert.ok(stripped.length <= 41, `line too long: ${stripped}`);
193
+ }
194
+ });
195
+
196
+ // ─── reset() ─────────────────────────────────────────────────────────
197
+
198
+ it("reset() clears all state", () => {
199
+ widget.startIteration(2, 5);
200
+ widget.setPhase("writing");
201
+ widget.recordWrite("src/a.ts");
202
+ widget.reset();
203
+ const lines = widget.renderWidget(80, stubTheme);
204
+ assert.match(lines[0], /idle/);
205
+ assert.ok(!lines.some(l => l.includes("a.ts")));
206
+ });
207
+
208
+ // ─── makeRenderer ─────────────────────────────────────────────────────
209
+
210
+ it("makeRenderer returns factory and invalidate", () => {
211
+ const { factory, invalidate } = widget.makeRenderer();
212
+ assert.equal(typeof factory, "function");
213
+ assert.equal(typeof invalidate, "function");
214
+ });
215
+
216
+ it("factory returns render + invalidate + dispose", () => {
217
+ const { factory } = widget.makeRenderer();
218
+ const comp = factory({} as any, stubTheme as any);
219
+ assert.equal(typeof comp.render, "function");
220
+ assert.equal(typeof comp.invalidate, "function");
221
+ assert.equal(typeof comp.dispose, "function");
222
+ });
223
+
224
+ it("factory render() returns widget lines", () => {
225
+ widget.setPhase("planning");
226
+ const { factory } = widget.makeRenderer();
227
+ const comp = factory({} as any, stubTheme as any);
228
+ const lines = comp.render(80);
229
+ assert.ok(Array.isArray(lines));
230
+ assert.ok(lines.length > 0);
231
+ });
232
+
233
+ // ─── getLoopWidget singleton ────────────────────────────────────────────
234
+
235
+ it("getLoopWidget returns the same instance", () => {
236
+ // Dynamic import to avoid module caching issues
237
+ import("./widget.js").then(m => {
238
+ const a = m.getLoopWidget();
239
+ const b = m.getLoopWidget();
240
+ assert.strictEqual(a, b);
241
+ });
242
+ });
243
+
244
+ });