pi-harness-runtime 1.1.49 → 1.1.71-3

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 (203) hide show
  1. package/CHANGELOG.md +30 -9
  2. package/README.md +119 -0
  3. package/harness/langchain/agents.d.ts +143 -0
  4. package/harness/langchain/agents.test.d.ts +2 -0
  5. package/harness/langchain/agents.test.ts +0 -1
  6. package/harness/langchain/agents.ts +196 -13
  7. package/harness/langchain/checkpointer.d.ts +48 -0
  8. package/harness/langchain/checkpointer.test.d.ts +8 -0
  9. package/harness/langchain/cron-cli.d.ts +12 -0
  10. package/harness/langchain/cron-cli.ts +169 -0
  11. package/harness/langchain/daemon.d.ts +161 -0
  12. package/harness/langchain/daemon.test.d.ts +11 -0
  13. package/harness/langchain/daemon.test.ts +62 -5
  14. package/harness/langchain/daemon.ts +231 -12
  15. package/harness/langchain/file-tools.d.ts +97 -0
  16. package/harness/langchain/file-tools.ts +29 -11
  17. package/harness/langchain/graph.d.ts +706 -0
  18. package/harness/langchain/graph.ts +184 -26
  19. package/harness/langchain/model-router.d.ts +63 -0
  20. package/harness/langchain/model-router.test.d.ts +2 -0
  21. package/harness/langchain/model-router.test.ts +191 -0
  22. package/harness/langchain/model-router.ts +188 -0
  23. package/harness/langchain/models.d.ts +72 -0
  24. package/harness/langchain/models.ts +111 -49
  25. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  26. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  27. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  28. package/harness/langchain/ping-pong-middleware.ts +204 -0
  29. package/harness/langchain/run.d.ts +15 -0
  30. package/harness/langchain/run.ts +15 -3
  31. package/harness/langchain/scoreboard.test.d.ts +13 -0
  32. package/harness/langchain/scoreboard.test.ts +28 -5
  33. package/harness/langchain/status-line.d.ts +91 -0
  34. package/harness/langchain/status-line.test.d.ts +8 -0
  35. package/harness/langchain/surge.d.ts +103 -0
  36. package/harness/langchain/surge.test.d.ts +10 -0
  37. package/harness/langchain/surge.test.ts +179 -0
  38. package/harness/langchain/surge.ts +188 -19
  39. package/harness/langchain/widget.d.ts +105 -0
  40. package/harness/langchain/widget.test.d.ts +8 -0
  41. package/harness/ssh-detach-interceptor.test.ts +263 -0
  42. package/harness/ssh-detach-interceptor.ts +438 -0
  43. package/index.ts +212 -47
  44. package/package.json +4 -2
  45. package/packages/a2a-adapter/package.json +1 -1
  46. package/packages/architecture-generator/package.json +1 -1
  47. package/packages/auth/package.json +1 -1
  48. package/packages/autonomous-refactor/package.json +1 -1
  49. package/packages/autonomous-runtime/package.json +1 -1
  50. package/packages/cache-strategy/package.json +1 -1
  51. package/packages/capability-registry/package.json +1 -1
  52. package/packages/checkpoint/package.json +1 -1
  53. package/packages/cli-plugin-sdk/package.json +1 -1
  54. package/packages/clipboard/package.json +1 -1
  55. package/packages/clipboard-plugin/package.json +1 -1
  56. package/packages/code-generation/package.json +1 -1
  57. package/packages/code-review/package.json +1 -1
  58. package/packages/codex-adapter/package.json +1 -1
  59. package/packages/config-capture/package.json +1 -1
  60. package/packages/context-compiler/package.json +1 -1
  61. package/packages/context-discovery/package.json +1 -1
  62. package/packages/context-manager/package.json +1 -1
  63. package/packages/cookie-sanitizer/package.json +1 -1
  64. package/packages/cost-optimizer/package.json +1 -1
  65. package/packages/dependency-analyzer/package.json +1 -1
  66. package/packages/django-plugin/package.json +1 -1
  67. package/packages/doc-generator/package.json +1 -1
  68. package/packages/evaluation-engine/package.json +1 -1
  69. package/packages/evaluation-runner/package.json +1 -1
  70. package/packages/event-bus/package.json +1 -1
  71. package/packages/event-store/package.json +1 -1
  72. package/packages/experience-replay/package.json +1 -1
  73. package/packages/feedback-collector/package.json +1 -1
  74. package/packages/file-copy-helper/package.json +1 -1
  75. package/packages/framework-detector/package.json +1 -1
  76. package/packages/framework-plugin-sdk/package.json +1 -1
  77. package/packages/frappe-plugin/package.json +1 -1
  78. package/packages/generic-web-plugin/package.json +1 -1
  79. package/packages/health-monitor/package.json +1 -1
  80. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  81. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  82. package/packages/intent-analyzer/dist/src/index.js +1 -0
  83. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  87. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  88. package/packages/intent-analyzer/package.json +1 -1
  89. package/packages/intent-analyzer/src/index.ts +13 -0
  90. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  91. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  92. package/packages/jev-judge/README.md +234 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  94. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  95. package/packages/jev-judge/dist/decision-engine.js +151 -0
  96. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  98. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  99. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  100. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  102. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  103. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  104. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  105. package/packages/jev-judge/dist/index.d.ts +77 -0
  106. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  107. package/packages/jev-judge/dist/index.js +271 -0
  108. package/packages/jev-judge/dist/index.js.map +1 -0
  109. package/packages/jev-judge/dist/types.d.ts +97 -0
  110. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  111. package/packages/jev-judge/dist/types.js +8 -0
  112. package/packages/jev-judge/dist/types.js.map +1 -0
  113. package/packages/jev-judge/package.json +46 -0
  114. package/packages/jev-judge/src/decision-engine.ts +231 -0
  115. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  116. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  117. package/packages/jev-judge/src/index.ts +345 -0
  118. package/packages/jev-judge/src/types.ts +131 -0
  119. package/packages/knowledge-graph/package.json +1 -1
  120. package/packages/knowledge-retrieval/package.json +1 -1
  121. package/packages/laravel-plugin/package.json +1 -1
  122. package/packages/learning-engine/package.json +1 -1
  123. package/packages/mcp-adapter/package.json +1 -1
  124. package/packages/memory-engine/package.json +1 -1
  125. package/packages/milestone-manager/package.json +1 -1
  126. package/packages/model-registry/package.json +1 -1
  127. package/packages/nextjs-plugin/package.json +1 -1
  128. package/packages/notification/package.json +1 -1
  129. package/packages/observability/package.json +1 -1
  130. package/packages/okf-indexer/package.json +1 -1
  131. package/packages/performance-optimizer/package.json +1 -1
  132. package/packages/privilege-broker/package.json +1 -1
  133. package/packages/project-analyzer/package.json +1 -1
  134. package/packages/project-bootstrap/package.json +1 -1
  135. package/packages/projection-engine/package.json +1 -1
  136. package/packages/prompt-compiler/package.json +1 -1
  137. package/packages/prompt-versioning/package.json +1 -1
  138. package/packages/provider-adapter-sdk/package.json +1 -1
  139. package/packages/provider-router/package.json +1 -1
  140. package/packages/provider-selector/package.json +1 -1
  141. package/packages/providers/package.json +1 -1
  142. package/packages/quota-manager/package.json +1 -1
  143. package/packages/rate-limiter/package.json +1 -1
  144. package/packages/react-vite-plugin/package.json +1 -1
  145. package/packages/release-manager/package.json +1 -1
  146. package/packages/requirement-compiler/package.json +1 -1
  147. package/packages/runtime/package.json +1 -1
  148. package/packages/scheduler/package.json +1 -1
  149. package/packages/scheduler-adapter/package.json +1 -1
  150. package/packages/session/package.json +1 -1
  151. package/packages/session-api/package.json +1 -1
  152. package/packages/session-export/package.json +1 -1
  153. package/packages/shared-context/package.json +1 -1
  154. package/packages/skill-mcp-client/package.json +1 -1
  155. package/packages/skill-registry/package.json +1 -1
  156. package/packages/sprint-planner/package.json +1 -1
  157. package/packages/subscription-engine/package.json +1 -1
  158. package/packages/task-compiler/package.json +1 -1
  159. package/packages/tencentdb-memory/package.json +1 -1
  160. package/packages/tencentdb-sync/package.json +1 -1
  161. package/packages/test-data-generator/package.json +1 -1
  162. package/packages/test-generator/package.json +1 -1
  163. package/packages/todo-bd-sync/README.md +71 -0
  164. package/packages/todo-bd-sync/package.json +1 -1
  165. package/packages/todo-bd-sync/src/extension.ts +16 -1
  166. package/packages/token-estimation/package.json +1 -1
  167. package/packages/token-optimizer/package.json +1 -1
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  169. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  171. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  172. package/packages/trajectory/dist/src/index.d.ts +13 -0
  173. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  174. package/packages/trajectory/dist/src/index.js +12 -0
  175. package/packages/trajectory/dist/src/index.js.map +1 -0
  176. package/packages/trajectory/dist/src/store.d.ts +69 -0
  177. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  178. package/packages/trajectory/dist/src/store.js +253 -0
  179. package/packages/trajectory/dist/src/store.js.map +1 -0
  180. package/packages/trajectory/dist/src/types.d.ts +73 -0
  181. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  182. package/packages/trajectory/dist/src/types.js +9 -0
  183. package/packages/trajectory/dist/src/types.js.map +1 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  185. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  186. package/packages/trajectory/dist/test/store.test.js +228 -0
  187. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  188. package/packages/trajectory/package.json +29 -0
  189. package/packages/trajectory/src/approved-patterns.ts +180 -0
  190. package/packages/trajectory/src/index.ts +26 -0
  191. package/packages/trajectory/src/store.ts +285 -0
  192. package/packages/trajectory/src/types.ts +84 -0
  193. package/packages/trajectory/test/store.test.d.ts +2 -0
  194. package/packages/trajectory/test/store.test.ts +250 -0
  195. package/packages/tui/package.json +1 -1
  196. package/packages/types/package.json +1 -1
  197. package/packages/workflow-events/package.json +1 -1
  198. package/packages/workspace-scanner/package.json +1 -1
  199. package/packages/worktree/package.json +1 -1
  200. package/packages/write-review/package.json +1 -1
  201. package/packages/write-review/src/blackboard.ts +38 -5
  202. package/packages/write-review/src/types.ts +13 -0
  203. package/skills/harness-runtime/SKILL.md +67 -4
@@ -0,0 +1,387 @@
1
+ import {
2
+ existsSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ statSync,
6
+ } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { IntentAnalyzer } from "./analyzer.js";
9
+ import type { Intent, IntentKind } from "./types.js";
10
+
11
+ export type PingPongDecision =
12
+ | "run_ping_pong"
13
+ | "suggest_ping_pong"
14
+ | "handle_inline";
15
+
16
+ export interface CodebaseComplexityInput {
17
+ /** Approximate total source/config/documentation files in the current repo. */
18
+ fileCount?: number;
19
+ /** Number of top-level packages/apps/workspaces. */
20
+ packageCount?: number;
21
+ /** Number of framework/runtime signals found, such as package.json, Cargo.toml, or pyproject.toml. */
22
+ frameworkSignalCount?: number;
23
+ /** Number of changed files in the working tree, if known. */
24
+ changedFileCount?: number;
25
+ /** Paths the user explicitly mentioned. */
26
+ mentionedPaths?: string[];
27
+ /** True when tests already exist and changes should usually preserve them. */
28
+ hasTests?: boolean;
29
+ /** True when release/build automation exists. */
30
+ hasCiOrRelease?: boolean;
31
+ }
32
+
33
+ export interface PingPongDecisionOptions {
34
+ /** Project-level complexity signals. Pass scanCodebaseComplexity(cwd) when available. */
35
+ codebase?: CodebaseComplexityInput;
36
+ /** Lower thresholds for fully autonomous sessions. */
37
+ autonomous?: boolean;
38
+ /** Allow exact user override words. Default: true. */
39
+ allowExplicitOverride?: boolean;
40
+ }
41
+
42
+ export interface PingPongSignal {
43
+ id: string;
44
+ label: string;
45
+ weight: number;
46
+ }
47
+
48
+ export interface PingPongDecisionResult {
49
+ decision: PingPongDecision;
50
+ score: number;
51
+ thresholds: {
52
+ run: number;
53
+ suggest: number;
54
+ };
55
+ intent: Intent;
56
+ signals: PingPongSignal[];
57
+ reasons: string[];
58
+ }
59
+
60
+ const CODE_CHANGE_INTENTS = new Set<IntentKind>([
61
+ "bug_fix",
62
+ "feature",
63
+ "refactor",
64
+ "testing",
65
+ "migration",
66
+ "security",
67
+ "performance",
68
+ "deployment",
69
+ ]);
70
+
71
+ const INLINE_INTENTS = new Set<IntentKind>([
72
+ "learning",
73
+ "research",
74
+ "code_review",
75
+ "documentation",
76
+ "general",
77
+ ]);
78
+
79
+ const ACTION_PATTERNS: Array<[RegExp, string, number]> = [
80
+ [/\b(implement|build|create|add|introduce)\b/i, "implementation request", 2],
81
+ [/\b(fix|debug|repair|patch|resolve)\b/i, "bug-fix request", 2],
82
+ [/\b(refactor|rewrite|restructure|migrate|convert|port)\b/i, "structural change request", 3],
83
+ [/\b(test|spec|coverage|e2e|integration)\b/i, "test-impacting request", 1],
84
+ [/\b(release|deploy|publish|ship)\b/i, "release-impacting request", 2],
85
+ ];
86
+
87
+ const COMPLEXITY_PATTERNS: Array<[RegExp, string, number]> = [
88
+ [/\b(system|architecture|daemon|runtime|orchestrator|scheduler|graph|loop|workflow|pipeline)\b/i, "system/architecture area", 3],
89
+ [/\b(concurrent|parallel|async|queue|event|bus|lease|worker|agent|multi[- ]?agent)\b/i, "coordination or concurrency", 3],
90
+ [/\b(cross[- ]?package|workspace|monorepo|package|plugin|extension)\b/i, "cross-package surface", 2],
91
+ [/\b(auth|token|secret|permission|security|quota|billing|cost)\b/i, "sensitive or quota-sensitive area", 2],
92
+ [/\b(database|migration|schema|cache|state|checkpoint|persistence)\b/i, "stateful change area", 2],
93
+ ];
94
+
95
+ const INLINE_PATTERNS: Array<[RegExp, string, number]> = [
96
+ [/\b(explain|what is|how does|why|analyze|investigate|understand)\b/i, "analysis-only wording", -4],
97
+ [/\b(no code|do not edit|don't edit|only explain|just explain|read only|read-only)\b/i, "explicit no-edit wording", -8],
98
+ [/\b(show|list|status|inspect|summarize)\b/i, "inspection wording", -2],
99
+ ];
100
+
101
+ const EXPLICIT_RUN = /\b(ping[- ]?pong|write[- ]?review|plan\s*->\s*cod(?:e|ing)|review\s*->\s*fix|write\s+(?:down\s+)?(?:the\s+)?plan\s+to\s+wiki|ask\s+minimax\s+to\s+(?:write|code|implement)|(?:then\s+)?you\s+review)\b/i;
102
+ const EXPLICIT_SKIP = /\b(no ping[- ]?pong|skip ping[- ]?pong|do not run ping[- ]?pong|don't run ping[- ]?pong)\b/i;
103
+
104
+ function pushSignal(
105
+ signals: PingPongSignal[],
106
+ id: string,
107
+ label: string,
108
+ weight: number,
109
+ ): void {
110
+ signals.push({ id, label, weight });
111
+ }
112
+
113
+ function countPathMentions(text: string): string[] {
114
+ const matches = text.match(
115
+ /(?:^|\s)((?:\.\/|\/)?(?:src|lib|packages|apps|harness|test|tests|docs|wiki|scripts|config|src-ui|src-tauri)[/\w.-]*\.[a-z0-9]+)/gi,
116
+ );
117
+ if (!matches) return [];
118
+ return matches.map((m) => m.trim());
119
+ }
120
+
121
+ function clampScore(score: number): number {
122
+ return Math.max(0, Math.min(20, score));
123
+ }
124
+
125
+ export class PingPongDecisionEngine {
126
+ private readonly intentAnalyzer: IntentAnalyzer;
127
+
128
+ constructor(intentAnalyzer = new IntentAnalyzer()) {
129
+ this.intentAnalyzer = intentAnalyzer;
130
+ }
131
+
132
+ analyze(
133
+ request: string,
134
+ options: PingPongDecisionOptions = {},
135
+ ): PingPongDecisionResult {
136
+ const allowOverride = options.allowExplicitOverride ?? true;
137
+ const intent = this.intentAnalyzer.analyze(request);
138
+ const signals: PingPongSignal[] = [];
139
+ let score = 0;
140
+
141
+ if (allowOverride && EXPLICIT_SKIP.test(request)) {
142
+ pushSignal(signals, "override.skip", "explicit skip request", -20);
143
+ } else if (allowOverride && EXPLICIT_RUN.test(request)) {
144
+ pushSignal(signals, "override.run", "explicit PING-PONG request", 20);
145
+ }
146
+
147
+ if (CODE_CHANGE_INTENTS.has(intent.kind)) {
148
+ const weight = intent.confidence === "high" ? 4 : 3;
149
+ pushSignal(signals, `intent.${intent.kind}`, `${intent.kind} intent`, weight);
150
+ } else if (INLINE_INTENTS.has(intent.kind)) {
151
+ const weight = intent.kind === "general" ? -3 : -2;
152
+ pushSignal(signals, `intent.${intent.kind}`, `${intent.kind} intent`, weight);
153
+ }
154
+
155
+ for (const [pattern, label, weight] of ACTION_PATTERNS) {
156
+ if (pattern.test(request)) pushSignal(signals, `action.${label}`, label, weight);
157
+ }
158
+
159
+ for (const [pattern, label, weight] of COMPLEXITY_PATTERNS) {
160
+ if (pattern.test(request)) pushSignal(signals, `complexity.${label}`, label, weight);
161
+ }
162
+
163
+ for (const [pattern, label, weight] of INLINE_PATTERNS) {
164
+ if (pattern.test(request)) pushSignal(signals, `inline.${label}`, label, weight);
165
+ }
166
+
167
+ const mentionedPaths = [
168
+ ...(options.codebase?.mentionedPaths ?? []),
169
+ ...countPathMentions(request),
170
+ ];
171
+ if (mentionedPaths.length >= 2) {
172
+ pushSignal(signals, "scope.paths.multi", "multiple files mentioned", 2);
173
+ } else if (mentionedPaths.length === 1) {
174
+ pushSignal(signals, "scope.paths.single", "specific file mentioned", 1);
175
+ }
176
+
177
+ this.addCodebaseSignals(signals, options.codebase);
178
+
179
+ score = clampScore(signals.reduce((sum, signal) => sum + signal.weight, 0));
180
+
181
+ const thresholds = {
182
+ run: options.autonomous ? 8 : 9,
183
+ suggest: options.autonomous ? 5 : 6,
184
+ };
185
+
186
+ let decision: PingPongDecision;
187
+ if (allowOverride && EXPLICIT_SKIP.test(request)) {
188
+ decision = "handle_inline";
189
+ score = 0;
190
+ } else if (allowOverride && EXPLICIT_RUN.test(request)) {
191
+ decision = "run_ping_pong";
192
+ score = Math.max(score, thresholds.run);
193
+ } else if (score >= thresholds.run) {
194
+ decision = "run_ping_pong";
195
+ } else if (score >= thresholds.suggest) {
196
+ decision = "suggest_ping_pong";
197
+ } else {
198
+ decision = "handle_inline";
199
+ }
200
+
201
+ return {
202
+ decision,
203
+ score,
204
+ thresholds,
205
+ intent,
206
+ signals,
207
+ reasons: signals
208
+ .filter((signal) => signal.weight !== 0)
209
+ .sort((a, b) => Math.abs(b.weight) - Math.abs(a.weight))
210
+ .map((signal) => `${signal.label} (${signal.weight > 0 ? "+" : ""}${signal.weight})`),
211
+ };
212
+ }
213
+
214
+ private addCodebaseSignals(
215
+ signals: PingPongSignal[],
216
+ codebase?: CodebaseComplexityInput,
217
+ ): void {
218
+ if (!codebase) return;
219
+
220
+ if ((codebase.fileCount ?? 0) >= 1000) {
221
+ pushSignal(signals, "repo.files.large", "large repository", 3);
222
+ } else if ((codebase.fileCount ?? 0) >= 250) {
223
+ pushSignal(signals, "repo.files.medium", "medium repository", 2);
224
+ }
225
+
226
+ if ((codebase.packageCount ?? 0) >= 5) {
227
+ pushSignal(signals, "repo.packages.large", "many packages/apps", 3);
228
+ } else if ((codebase.packageCount ?? 0) >= 2) {
229
+ pushSignal(signals, "repo.packages.multi", "multi-package project", 2);
230
+ }
231
+
232
+ if ((codebase.frameworkSignalCount ?? 0) >= 4) {
233
+ pushSignal(signals, "repo.frameworks.many", "many framework/runtime signals", 2);
234
+ }
235
+
236
+ if ((codebase.changedFileCount ?? 0) >= 8) {
237
+ pushSignal(signals, "repo.changes.many", "many current changes", 2);
238
+ }
239
+
240
+ if (codebase.hasTests) {
241
+ pushSignal(signals, "repo.tests", "test suite present", 1);
242
+ }
243
+
244
+ if (codebase.hasCiOrRelease) {
245
+ pushSignal(signals, "repo.release", "release/build automation present", 1);
246
+ }
247
+ }
248
+ }
249
+
250
+ export interface ScanCodebaseOptions {
251
+ maxFiles?: number;
252
+ maxDepth?: number;
253
+ }
254
+
255
+ const SKIP_DIRS = new Set([
256
+ ".git",
257
+ "node_modules",
258
+ "dist",
259
+ "build",
260
+ "coverage",
261
+ ".next",
262
+ ".turbo",
263
+ "target",
264
+ ]);
265
+
266
+ const FRAMEWORK_FILES = new Set([
267
+ "package.json",
268
+ "tsconfig.json",
269
+ "Cargo.toml",
270
+ "pyproject.toml",
271
+ "composer.json",
272
+ "go.mod",
273
+ "bun.lock",
274
+ "pnpm-lock.yaml",
275
+ "vite.config.ts",
276
+ "next.config.js",
277
+ "tauri.conf.json",
278
+ ]);
279
+
280
+ export function scanCodebaseComplexity(
281
+ root: string,
282
+ options: ScanCodebaseOptions = {},
283
+ ): CodebaseComplexityInput {
284
+ const maxFiles = options.maxFiles ?? 4000;
285
+ const maxDepth = options.maxDepth ?? 6;
286
+ let fileCount = 0;
287
+ let packageCount = 0;
288
+ let frameworkSignalCount = 0;
289
+ let hasTests = false;
290
+ let hasCiOrRelease = false;
291
+
292
+ function visit(dir: string, depth: number): void {
293
+ if (fileCount >= maxFiles || depth > maxDepth) return;
294
+ let entries: string[];
295
+ try {
296
+ entries = readdirSync(dir);
297
+ } catch {
298
+ return;
299
+ }
300
+
301
+ for (const entry of entries) {
302
+ if (fileCount >= maxFiles) return;
303
+ const path = join(dir, entry);
304
+ let stats;
305
+ try {
306
+ stats = statSync(path);
307
+ } catch {
308
+ continue;
309
+ }
310
+
311
+ if (stats.isDirectory()) {
312
+ if (SKIP_DIRS.has(entry)) continue;
313
+ if ((entry === "packages" || entry === "apps") && depth <= 1) {
314
+ packageCount += countChildDirs(path);
315
+ }
316
+ if (entry === "test" || entry === "tests" || entry === "__tests__") {
317
+ hasTests = true;
318
+ }
319
+ visit(path, depth + 1);
320
+ continue;
321
+ }
322
+
323
+ if (!stats.isFile()) continue;
324
+ fileCount += 1;
325
+
326
+ if (FRAMEWORK_FILES.has(entry)) frameworkSignalCount += 1;
327
+ if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(entry)) hasTests = true;
328
+ if (
329
+ entry === "package.json" &&
330
+ readJsonHasPackageScripts(path, ["build", "release", "publish"])
331
+ ) {
332
+ hasCiOrRelease = true;
333
+ }
334
+ if (
335
+ path.includes("/.github/workflows/") ||
336
+ entry === "release.yml" ||
337
+ entry === "release.yaml"
338
+ ) {
339
+ hasCiOrRelease = true;
340
+ }
341
+ }
342
+ }
343
+
344
+ visit(root, 0);
345
+
346
+ return {
347
+ fileCount,
348
+ packageCount,
349
+ frameworkSignalCount,
350
+ hasTests,
351
+ hasCiOrRelease,
352
+ };
353
+ }
354
+
355
+ function countChildDirs(dir: string): number {
356
+ try {
357
+ return readdirSync(dir).filter((entry) => {
358
+ try {
359
+ return statSync(join(dir, entry)).isDirectory();
360
+ } catch {
361
+ return false;
362
+ }
363
+ }).length;
364
+ } catch {
365
+ return 0;
366
+ }
367
+ }
368
+
369
+ function readJsonHasPackageScripts(path: string, scriptNames: string[]): boolean {
370
+ if (!existsSync(path)) return false;
371
+ try {
372
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as {
373
+ scripts?: Record<string, unknown>;
374
+ };
375
+ const scripts = parsed.scripts ?? {};
376
+ return scriptNames.some((name) => typeof scripts[name] === "string");
377
+ } catch {
378
+ return false;
379
+ }
380
+ }
381
+
382
+ export function analyzePingPongDecision(
383
+ request: string,
384
+ options: PingPongDecisionOptions = {},
385
+ ): PingPongDecisionResult {
386
+ return new PingPongDecisionEngine().analyze(request, options);
387
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, it } from "node:test";
2
+ import { equal, ok } from "node:assert";
3
+ import {
4
+ PingPongDecisionEngine,
5
+ analyzePingPongDecision,
6
+ scanCodebaseComplexity,
7
+ } from "../src/ping-pong.js";
8
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+
12
+ describe("PingPongDecisionEngine", () => {
13
+ const engine = new PingPongDecisionEngine();
14
+
15
+ it("runs PING-PONG for explicit plan-code-review phrasing", () => {
16
+ const result = engine.analyze(
17
+ "Good plan, then write down plan to wiki/ then I will ask Minimax to write code then you review",
18
+ );
19
+
20
+ equal(result.decision, "run_ping_pong");
21
+ ok(result.reasons.some((reason) => reason.includes("explicit PING-PONG")));
22
+ });
23
+
24
+ it("runs PING-PONG for complex runtime implementation work", () => {
25
+ const result = engine.analyze("implement daemon graph loop recovery across packages", {
26
+ codebase: {
27
+ fileCount: 1200,
28
+ packageCount: 12,
29
+ frameworkSignalCount: 8,
30
+ hasTests: true,
31
+ hasCiOrRelease: true,
32
+ },
33
+ });
34
+
35
+ equal(result.decision, "run_ping_pong");
36
+ ok(result.score >= result.thresholds.run);
37
+ });
38
+
39
+ it("keeps analysis-only questions inline", () => {
40
+ const result = engine.analyze("analyze why the loop is quiet, do not edit code");
41
+
42
+ equal(result.decision, "handle_inline");
43
+ equal(result.score, 0);
44
+ });
45
+
46
+ it("can suggest PING-PONG for borderline code work", () => {
47
+ const result = engine.analyze("add tests for parser", {
48
+ codebase: { hasTests: true },
49
+ });
50
+
51
+ equal(result.decision, "suggest_ping_pong");
52
+ ok(result.score >= result.thresholds.suggest);
53
+ ok(result.score < result.thresholds.run);
54
+ });
55
+
56
+ it("allows explicit skip to override code-change wording", () => {
57
+ const result = engine.analyze("fix the daemon bug but skip ping-pong for now");
58
+
59
+ equal(result.decision, "handle_inline");
60
+ equal(result.score, 0);
61
+ });
62
+ });
63
+
64
+ describe("scanCodebaseComplexity", () => {
65
+ it("extracts package, test, and release signals", () => {
66
+ const root = mkdtempSync(join(tmpdir(), "ping-pong-scan-"));
67
+ try {
68
+ mkdirSync(join(root, "packages", "alpha"), { recursive: true });
69
+ mkdirSync(join(root, "packages", "beta"), { recursive: true });
70
+ mkdirSync(join(root, "test"), { recursive: true });
71
+ writeFileSync(
72
+ join(root, "package.json"),
73
+ JSON.stringify({ scripts: { build: "tsc", release: "standard-version" } }),
74
+ );
75
+ writeFileSync(join(root, "tsconfig.json"), "{}", "utf8");
76
+ writeFileSync(join(root, "test", "demo.test.ts"), "", "utf8");
77
+
78
+ const result = scanCodebaseComplexity(root);
79
+
80
+ ok(result.fileCount >= 3);
81
+ equal(result.packageCount, 2);
82
+ ok(result.frameworkSignalCount >= 2);
83
+ equal(result.hasTests, true);
84
+ equal(result.hasCiOrRelease, true);
85
+ } finally {
86
+ rmSync(root, { recursive: true, force: true });
87
+ }
88
+ });
89
+ });
90
+
91
+ describe("analyzePingPongDecision", () => {
92
+ it("uses the default engine", () => {
93
+ const result = analyzePingPongDecision("ping-pong this implementation");
94
+ equal(result.decision, "run_ping_pong");
95
+ });
96
+ });
@@ -0,0 +1,234 @@
1
+ # Jev Judge
2
+
3
+ Structured decision model integration for pi-harness-runtime. Uses [TypeSafe Jev](https://typesafe.ai) via OpenRouter for fast, calibrated, hallucination-free decisions.
4
+
5
+ ## Overview
6
+
7
+ Jev is a "System One" model - a new class of AI optimized for **structured decisions** rather than text generation:
8
+
9
+ | Aspect | Traditional LLM | Jev (System One) |
10
+ |--------|-----------------|------------------|
11
+ | Output | Text (can hallucinate) | Type-safe values (0% hallucination) |
12
+ | Speed | ~1-2s per call | **~250ms per call** |
13
+ | Cost | Higher | ~5x cheaper |
14
+ | Use case | Chat, reasoning | Classification, routing, decisions |
15
+
16
+ ## Features
17
+
18
+ - **JevJudge** - Core API for structured decisions
19
+ - **E2EJudge** - E2E testing decisions (flaky detection, build blocking, retry)
20
+ - **FlakyDetector** - Specialized flaky test detection
21
+ - **DecisionEngine** - Threshold-based routing and actions
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # Via monorepo (already included)
27
+ bun install
28
+
29
+ # Or install separately
30
+ npm install @pi-harness/jev-judge
31
+ ```
32
+
33
+ ## Configuration
34
+
35
+ Requires OpenRouter API key with TypeSafe/Jev access:
36
+
37
+ ```bash
38
+ export OPENROUTER_API_KEY="sk-or-..."
39
+ ```
40
+
41
+ Or set in code:
42
+
43
+ ```typescript
44
+ const judge = new JevJudge({
45
+ apiKey: process.env.OPENROUTER_API_KEY!,
46
+ baseUrl: "https://openrouter.ai/api/v1", // default
47
+ model: "typesafe/jev-1.13", // default
48
+ });
49
+ ```
50
+
51
+ ## Usage
52
+
53
+ ### Basic Decisions
54
+
55
+ ```typescript
56
+ import { JevJudge } from "@pi-harness/jev-judge";
57
+
58
+ const judge = new JevJudge({ apiKey: process.env.OPENROUTER_API_KEY! });
59
+
60
+ // Ask multiple questions at once
61
+ const result = await judge.evaluate(
62
+ "The deploy failed with exit code 1. Error: ECONNREFUSED",
63
+ {
64
+ isUrgent: {
65
+ type: "noul",
66
+ instructions: "Does this require immediate attention?"
67
+ },
68
+ severity: {
69
+ type: "choice",
70
+ instructions: "What is the severity level?",
71
+ options: ["critical", "high", "medium", "low"]
72
+ }
73
+ }
74
+ );
75
+
76
+ console.log(result.decisions.isUrgent.probability); // 0.92
77
+ console.log(result.decisions.severity.response.choice); // "critical"
78
+ ```
79
+
80
+ ### Task Tracker Routing (Todo vs BD)
81
+
82
+ ```typescript
83
+ import { Router } from "@pi-harness/jev-judge/decision-engine";
84
+
85
+ const result = await Router.routeTaskTracker(
86
+ "Fix the login bug on the checkout page",
87
+ judge
88
+ );
89
+
90
+ if (result.tracker === "bd") {
91
+ // Create GitHub issue
92
+ } else {
93
+ // Use local todo
94
+ }
95
+ ```
96
+
97
+ ### E2E Test Flaky Detection
98
+
99
+ ```typescript
100
+ import { E2EJudge } from "@pi-harness/jev-judge/e2e-judge";
101
+
102
+ const e2eJudge = new E2EJudge({ apiKey: process.env.OPENROUTER_API_KEY! });
103
+
104
+ // Analyze a test failure
105
+ const flakyAnalysis = await e2eJudge.analyzeFlakiness({
106
+ name: "checkout-flow.spec.ts",
107
+ status: "failed",
108
+ error: "Timeout: Element #checkout-button not visible after 5000ms",
109
+ retries: 1
110
+ });
111
+
112
+ if (flakyAnalysis.isFlaky) {
113
+ console.log(`Flaky detected: ${flakyAnalysis.pattern}`);
114
+ // Auto-skip or retry
115
+ }
116
+ ```
117
+
118
+ ### Build Decision
119
+
120
+ ```typescript
121
+ // Should this failure block the build?
122
+ const blockDecision = await e2eJudge.shouldBlockBuild(testResult);
123
+
124
+ if (blockDecision.shouldBlock) {
125
+ console.log(`Blocking build: ${blockDecision.reason}`);
126
+ process.exit(1);
127
+ }
128
+ ```
129
+
130
+ ### Threshold-Based Actions
131
+
132
+ ```typescript
133
+ import { DecisionEngine } from "@pi-harness/jev-judge/decision-engine";
134
+
135
+ const engine = new DecisionEngine(
136
+ { apiKey: process.env.OPENROUTER_API_KEY! },
137
+ { approval: 0.9, review: 0.6, block: 0.3 }
138
+ );
139
+
140
+ const decision = engine.decide(0.85);
141
+
142
+ switch (decision.action) {
143
+ case "proceed":
144
+ // Auto-approve
145
+ break;
146
+ case "review":
147
+ // Send to human
148
+ break;
149
+ case "block":
150
+ // Auto-reject
151
+ break;
152
+ }
153
+ ```
154
+
155
+ ## API Reference
156
+
157
+ ### JevJudge
158
+
159
+ Core class for making structured decisions.
160
+
161
+ ```typescript
162
+ const judge = new JevJudge(config: JevJudgeConfig);
163
+ ```
164
+
165
+ #### Methods
166
+
167
+ - `evaluate(state, questions)` - Evaluate state against questions
168
+ - `noul(state, question)` - Quick yes/no evaluation
169
+ - `choice(state, question, options)` - Quick choice evaluation
170
+ - `thresholdDecision(probability, threshold)` - Threshold-based decision
171
+ - `batchEvaluate(states, questions)` - Batch evaluation
172
+
173
+ ### E2EJudge
174
+
175
+ Specialized judge for E2E testing.
176
+
177
+ ```typescript
178
+ const e2eJudge = new E2EJudge(config, options);
179
+ ```
180
+
181
+ #### Methods
182
+
183
+ - `analyzeFlakiness(testResult)` - Detect flaky tests
184
+ - `shouldBlockBuild(testResult)` - Build blocking decision
185
+ - `shouldRetry(testResult)` - Retry recommendation
186
+ - `classifyError(errorMessage)` - Error classification
187
+ - `batchAnalyze(testResults)` - Batch test analysis
188
+
189
+ ### FlakyDetector
190
+
191
+ Specialized for flaky test detection with pattern matching.
192
+
193
+ ```typescript
194
+ const detector = new FlakyDetector(config, threshold);
195
+ ```
196
+
197
+ #### Methods
198
+
199
+ - `detectPattern(errorMessage)` - Fast pattern-based detection
200
+ - `detect(testResult)` - AI-powered detection
201
+ - `batchDetect(testResults)` - Batch detection
202
+
203
+ ### DecisionEngine
204
+
205
+ Threshold-based routing and actions.
206
+
207
+ ```typescript
208
+ const engine = new DecisionEngine(config, thresholds);
209
+ ```
210
+
211
+ #### Methods
212
+
213
+ - `decide(probability)` - Decide action from probability
214
+ - `decideWithThresholds(probabilities)` - Multi-question decisions
215
+ - `route(decision, handlers)` - Route to handlers
216
+ - `batchDecide(decisions)` - Batch decisions
217
+ - `weightedVote(votes)` - Weighted voting
218
+
219
+ ## Pricing
220
+
221
+ Jev via OpenRouter:
222
+ - Input: $0.042 / 1M tokens
223
+ - Output: Free
224
+ - Latency: ~250ms P50
225
+
226
+ See [OpenRouter pricing](https://openrouter.ai/typesafe/jev-1.13) for details.
227
+
228
+ ## Examples
229
+
230
+ See `tests/` directory for example usage.
231
+
232
+ ## License
233
+
234
+ MIT