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
@@ -20,13 +20,15 @@ import {
20
20
  StateGraph,
21
21
  } from "@langchain/langgraph";
22
22
  import {
23
- autonomyDirective,
24
23
  isAutonomyRequest,
24
+ autonomyDirective,
25
25
  lastMessage,
26
26
  type ReviewVerdict,
27
27
  ReviewVerdictSchema,
28
28
  } from "./agents.js";
29
+ import { readFileSync } from "node:fs";
29
30
  import { WriteReviewBlackboard } from "../../packages/write-review/src/blackboard.js";
31
+ import { getApprovedPatternStore } from "../../packages/trajectory/src/index.js";
30
32
  import type { Verdict } from "../../packages/write-review/src/types.js";
31
33
  import type { LoopWidget } from "./widget.js";
32
34
 
@@ -51,6 +53,17 @@ const LoopState = Annotation.Root({
51
53
  reducer: (prev, next) => [...(prev ?? []), ...(next ?? [])],
52
54
  default: () => [],
53
55
  }),
56
+ /** Code from the previous iteration. Used for stuck detection (P0-2). */
57
+ prevCode: Annotation<string | undefined>({
58
+ reducer: (_prev, next) => next,
59
+ default: () => undefined,
60
+ }),
61
+ /** Actual file contents written in the most recent write step.
62
+ * P1-1: used to pass real file contents to the reviewer instead of a summary. */
63
+ writtenFiles: Annotation<Record<string, string>>({
64
+ reducer: (_prev, next) => next,
65
+ default: () => ({}),
66
+ }),
54
67
  });
55
68
 
56
69
  export type LoopState = typeof LoopState.State;
@@ -60,7 +73,22 @@ export type LoopState = typeof LoopState.State;
60
73
  export interface LoopDeps {
61
74
  plan: (request: string) => Promise<string>;
62
75
  write: (plan: string, review: ReviewVerdict | null) => Promise<string>;
63
- review: (plan: string, code: string) => Promise<ReviewVerdict>;
76
+ review: (
77
+ plan: string,
78
+ code: string,
79
+ writtenFiles?: Record<string, string>,
80
+ ) => Promise<ReviewVerdict>;
81
+ /**
82
+ * ADK ParallelAgent equivalent: run 3 specialist reviews in parallel.
83
+ * If provided, reviewNode uses this instead of the monolithic review().
84
+ * Specialists: security auditor + style auditor + performance analyst.
85
+ * Takes ~1/3 the time of sequential reviews.
86
+ */
87
+ parallelReview?: (
88
+ plan: string,
89
+ code: string,
90
+ writtenFiles?: Record<string, string>,
91
+ ) => Promise<ReviewVerdict>;
64
92
  maxIterations: number;
65
93
  onStep?: (step: string, state: LoopState) => void;
66
94
  /** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
@@ -81,18 +109,60 @@ function planNode(deps: LoopDeps) {
81
109
  };
82
110
  }
83
111
 
112
+ /** Extract file paths from a code string (mirrors blackboard.extractFilePaths). */
113
+ function extractFilePaths(code: string): string[] {
114
+ const files = new Set<string>();
115
+ const lines = code.split("\n");
116
+ for (const line of lines) {
117
+ const match = line.match(
118
+ /^```(?:typescript|ts|javascript|js|tsx|jsx|text)?\s*\/([^\s`]+)\s*$/,
119
+ );
120
+ if (match) {
121
+ const path = match[1].trim();
122
+ if (path && !path.includes(" ")) files.add(path);
123
+ }
124
+ const mdMatch = line.match(/^\*\*([^:*]+):\*\*/);
125
+ if (mdMatch) {
126
+ const path = mdMatch[1].trim();
127
+ if (
128
+ path &&
129
+ !path.includes(" ") &&
130
+ (path.includes("/") || path.endsWith(".ts") || path.endsWith(".js"))
131
+ ) {
132
+ files.add(path);
133
+ }
134
+ }
135
+ }
136
+ return [...files];
137
+ }
138
+
84
139
  function writeNode(deps: LoopDeps) {
85
140
  return async (state: LoopState): Promise<Partial<LoopState>> => {
86
141
  const code = await deps.write(state.plan, state.review ?? null);
87
142
  const iter = state.iteration + 1;
88
143
  deps.onStep?.("write", { ...state, iteration: iter });
144
+ // P1-1: read actual file contents so the reviewer sees real code, not a summary
145
+ const paths = extractFilePaths(code);
146
+ const writtenFiles: Record<string, string> = {};
147
+ for (const p of paths) {
148
+ try {
149
+ writtenFiles[p] = readFileSync(p, "utf8");
150
+ } catch {
151
+ // File may not exist yet (mkdir was pending); skip
152
+ }
153
+ }
89
154
  const reviewNote = state.review
90
155
  ? ` (addressing ${state.review.comments.length} review comment(s))`
91
156
  : "";
92
- // Note: previous `review` stays in state on purpose — the coder reads its comments.
93
157
  return {
94
158
  code,
159
+ writtenFiles,
95
160
  iteration: iter,
161
+ // Snapshot the code from the previous iteration (before it was
162
+ // overwritten). routeAfterReview uses this to detect stuck: if the
163
+ // coder outputs identical content in two consecutive iterations, the
164
+ // loop terminates early instead of wasting another review cycle.
165
+ prevCode: state.code,
96
166
  log: [`[write:${iter}] MiniMax wrote code${reviewNote}`],
97
167
  };
98
168
  };
@@ -100,7 +170,14 @@ function writeNode(deps: LoopDeps) {
100
170
 
101
171
  function reviewNode(deps: LoopDeps) {
102
172
  return async (state: LoopState): Promise<Partial<LoopState>> => {
103
- const review = await deps.review(state.plan, state.code);
173
+ // ADK ParallelAgent: use parallel review if available (3x faster)
174
+ // Falls back to sequential review for dry-run or when parallelReview is not wired.
175
+ const doReview = deps.parallelReview ?? deps.review;
176
+ const review = await doReview(
177
+ state.plan,
178
+ state.code,
179
+ state.writtenFiles ?? {},
180
+ );
104
181
  deps.onStep?.("review", state);
105
182
  return {
106
183
  review,
@@ -109,15 +186,25 @@ function reviewNode(deps: LoopDeps) {
109
186
  };
110
187
  }
111
188
 
112
- function finishNode() {
189
+ function finishNode(maxIterations: number) {
113
190
  return async (state: LoopState): Promise<Partial<LoopState>> => {
114
191
  const verdict = state.review?.verdict ?? "blocked";
115
- const reason =
116
- verdict === "approved"
117
- ? "reviewer approved"
118
- : verdict === "changes_requested"
119
- ? `max iterations (${state.iteration}) reached with changes still requested`
120
- : "reviewer blocked the task";
192
+ const comments = state.review?.comments ?? [];
193
+ let reason: string;
194
+ if (verdict === "approved") {
195
+ reason = "reviewer approved";
196
+ } else if (verdict === "blocked") {
197
+ reason = "reviewer blocked the task";
198
+ } else if (state.iteration >= maxIterations) {
199
+ reason = `max iterations (${state.iteration}) reached with changes still requested`;
200
+ } else if (
201
+ comments.length > 0 &&
202
+ comments.every((c) => c.severity === "minor" || !c.severity)
203
+ ) {
204
+ reason = `converged: only minor comments (${state.iteration})`;
205
+ } else {
206
+ reason = `stuck: changes still requested after ${state.iteration} iteration(s)`;
207
+ }
121
208
  return {
122
209
  log: [`[finish] ${state.iteration} iteration(s) — ${reason}`],
123
210
  };
@@ -126,13 +213,28 @@ function finishNode() {
126
213
 
127
214
  // ─── Routing ────────────────────────────────────────────────────────────────
128
215
 
216
+ /** Smart stop: loop terminates early when quality criteria are met. */
129
217
  function routeAfterReview(
130
218
  state: LoopState,
131
219
  maxIterations: number,
132
220
  ): "write" | "finish" {
133
221
  const verdict = state.review?.verdict;
222
+ const comments = state.review?.comments ?? [];
223
+ // 1. Always finish on approved or blocked
134
224
  if (verdict === "approved" || verdict === "blocked") return "finish";
225
+ // 2. Hard cap
135
226
  if (state.iteration >= maxIterations) return "finish";
227
+ // 3. SMART STOP: only minor comments (≤2) — converge fast
228
+ const hasOnlyMinor = comments.every(
229
+ (c) => c.severity === "minor" || !c.severity,
230
+ );
231
+ if (hasOnlyMinor && comments.length <= 2) return "finish";
232
+ // 4. P0-2 STUCK DETECTION: if the coder produced identical output in consecutive
233
+ // iterations, no progress is being made — terminate early instead of wasting
234
+ // another review cycle
235
+ if (state.prevCode !== undefined && state.code === state.prevCode) {
236
+ return "finish";
237
+ }
136
238
  return "write";
137
239
  }
138
240
 
@@ -148,7 +250,7 @@ export function buildWriteReviewLoop(
148
250
  .addNode("planStep", planNode(deps))
149
251
  .addNode("writeStep", writeNode(deps))
150
252
  .addNode("reviewStep", reviewNode(deps))
151
- .addNode("finishStep", finishNode())
253
+ .addNode("finishStep", finishNode(deps.maxIterations))
152
254
  .addEdge(START, "planStep")
153
255
  .addEdge("planStep", "writeStep")
154
256
  .addEdge("writeStep", "reviewStep")
@@ -182,6 +284,15 @@ export interface RealLoopOptions {
182
284
  onStep?: (step: string, state: LoopState) => void;
183
285
  /** Directory for .write-review/blackboard. Defaults to process.cwd(). */
184
286
  blackboardDir?: string;
287
+ /**
288
+ * The request string. Used by the smart model router to determine which
289
+ * models to use for planner / reviewer / coder roles. Supports per-request
290
+ * directives in the prompt:
291
+ * [planner: gpt] [reviewer: GLM] [coder: MiniMax]
292
+ * "Use GPT as master reviewer" is also recognised.
293
+ * If omitted, defaults to env vars (PLANNER_*, GLM_*, MINIMAX_*).
294
+ */
295
+ request?: string;
185
296
  importAgents?: () => Promise<{
186
297
  createPlannerAgent: () => {
187
298
  invoke: (input: { messages: unknown[] }) => Promise<{
@@ -214,9 +325,27 @@ export async function buildRealLoopDeps(
214
325
  options: RealLoopOptions = {},
215
326
  ): Promise<LoopDeps> {
216
327
  const mod = await import("./agents.js");
217
- const planner = mod.createPlannerAgent();
218
- const coder = mod.createCoderAgent();
219
- const reviewer = mod.createReviewerAgent();
328
+
329
+ // Smart model routing — parses [role: family] directives in the request.
330
+ // Falls back to env defaults if no directives present.
331
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
332
+ const { routeRequestSmart } = require("./model-router.js");
333
+ const route = options.request ? routeRequestSmart(options.request) : undefined;
334
+
335
+ const plannerModelOpts = route?.planner;
336
+ const reviewerModelOpts = route?.reviewer;
337
+ const coderModelOpts = route?.coder;
338
+
339
+ const planner = mod.createPlannerAgent(plannerModelOpts);
340
+ const coder = mod.createCoderAgent(coderModelOpts);
341
+ const reviewer = mod.createReviewerAgent(reviewerModelOpts);
342
+
343
+ // ADK ParallelAgent: create 3 specialist reviewers for parallel execution
344
+ const specialists = {
345
+ security: mod.createSpecialistReviewer("security", reviewerModelOpts),
346
+ style: mod.createSpecialistReviewer("style", reviewerModelOpts),
347
+ performance: mod.createSpecialistReviewer("performance", reviewerModelOpts),
348
+ };
220
349
 
221
350
  // Create and init the shared blackboard
222
351
  const blackboard = new WriteReviewBlackboard(
@@ -228,9 +357,8 @@ export async function buildRealLoopDeps(
228
357
  blackboard.init();
229
358
  }
230
359
 
231
- // Autonomy mode: computed inside plan(), stored for write()/review()
232
- const _autonomous = false;
233
- const _directive = "";
360
+ // Autonomy mode: set in plan() based on request content, reused by write()/review()
361
+ let _directive = "";
234
362
 
235
363
  /** Inject scoreboard markdown into a user message. */
236
364
  const withScoreboard = (msg: string) => {
@@ -238,6 +366,16 @@ export async function buildRealLoopDeps(
238
366
  return msg + "\n\n## Current Progress\n" + scoreboard;
239
367
  };
240
368
 
369
+ /** Inject scoreboard + approved patterns into a user message (for the reviewer). */
370
+ const withReviewContext = (msg: string) => {
371
+ const scoreboard = blackboard.toMarkdown();
372
+ const patterns = getApprovedPatternStore().toMarkdown();
373
+ const parts = [msg];
374
+ if (scoreboard) parts.push("\n\n## Current Progress\n" + scoreboard);
375
+ if (patterns) parts.push("\n\n" + patterns);
376
+ return parts.join("");
377
+ };
378
+
241
379
  // Wrap onStep to also update the blackboard
242
380
  const originalOnStep = options.onStep;
243
381
  const onStep = (step: string, state: LoopState) => {
@@ -280,14 +418,17 @@ export async function buildRealLoopDeps(
280
418
  return {
281
419
  maxIterations: options.maxIterations ?? 3,
282
420
  onStep,
283
- plan: async (request) =>
284
- lastMessage(
421
+ plan: async (request) => {
422
+ // P1-2: detect autonomy signal in the request and unlock full autonomous mode
423
+ _directive = isAutonomyRequest(request) ? autonomyDirective() : "";
424
+ return lastMessage(
285
425
  await planner.invoke({
286
426
  messages: [
287
427
  { role: "user", content: _directive + withScoreboard(request) },
288
428
  ],
289
429
  }),
290
- ),
430
+ );
431
+ },
291
432
  write: async (plan, review) => {
292
433
  const userMsg = review
293
434
  ? `## Plan\n${plan}\n\n## Review comments to address\n${review.comments
@@ -302,14 +443,20 @@ export async function buildRealLoopDeps(
302
443
  }),
303
444
  );
304
445
  },
305
- review: async (plan, code) => {
446
+ review: async (plan, _code, writtenFiles = {}) => {
447
+ // P1-1: build a code-to-review section from actual file contents
448
+ const codeSection =
449
+ Object.keys(writtenFiles).length > 0
450
+ ? Object.entries(writtenFiles)
451
+ .map(([path, content]) => `## ${path}\n\n\`\`\`\n${content}\n\`\`\``)
452
+ .join("\n\n")
453
+ : `## Code\n\n\`\`\`\n${_code}\n\`\`\``;
306
454
  const result = await reviewer.invoke({
307
455
  messages: [
308
456
  {
309
457
  role: "user",
310
458
  content:
311
- _directive +
312
- withScoreboard(`## Plan\n${plan}\n\n## Code to review\n${code}`),
459
+ _directive + withReviewContext(`## Plan\n${plan}\n\n${codeSection}`),
313
460
  },
314
461
  ],
315
462
  });
@@ -325,6 +472,9 @@ export async function buildRealLoopDeps(
325
472
  };
326
473
  }
327
474
  },
475
+ // ADK ParallelAgent: 3 specialists run in parallel via Promise.all
476
+ parallelReview: async (plan, _code, writtenFiles = {}) =>
477
+ mod.parallelReview(specialists, plan, _code, writtenFiles),
328
478
  };
329
479
  }
330
480
 
@@ -406,7 +556,7 @@ export function buildDryRunDeps(
406
556
  `\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``,
407
557
  );
408
558
  },
409
- review: async (_plan, _code) => {
559
+ review: async (_plan, _code, _writtenFiles = {}) => {
410
560
  if (writeCount < 2) {
411
561
  return {
412
562
  verdict: "changes_requested",
@@ -415,7 +565,7 @@ export function buildDryRunDeps(
415
565
  {
416
566
  file: "index.ts",
417
567
  comment: "dry-run: rename the exported constant",
418
- severity: "minor",
568
+ severity: "major",
419
569
  },
420
570
  ],
421
571
  };
@@ -426,5 +576,13 @@ export function buildDryRunDeps(
426
576
  comments: [],
427
577
  };
428
578
  },
579
+ // Dry-run parallel review: same deterministic stub (no LLM calls)
580
+ parallelReview: async (_plan, _code, _writtenFiles = {}) => {
581
+ return buildDryRunDeps({ maxIterations: options.maxIterations }).review(
582
+ "",
583
+ "",
584
+ {},
585
+ );
586
+ },
429
587
  };
430
588
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Smart model router — reads the request prompt and decides which model to use
3
+ * for each agent role at runtime. No hardcoded model IDs; all choices are driven
4
+ * by env vars + optional per-request directives in the prompt.
5
+ *
6
+ * Per-request directives (optional — fallback to env defaults if absent):
7
+ *
8
+ * Planner directive:
9
+ * [planner: gpt-5.6] or [planner: GLM-5.2] or [planner: MiniMax-M2]
10
+ *
11
+ * Reviewer directive (master reviewer — GLM is the design default):
12
+ * [reviewer: GLM-5.2] or [reviewer: GPT-5.6]
13
+ *
14
+ * Coder directive:
15
+ * [coder: MiniMax-M2] or [coder: gpt-5.6]
16
+ *
17
+ * Examples in a prompt:
18
+ * "Implement X [reviewer: GPT-5.6] [planner: GLM-5.2]"
19
+ * "Use GLM as master reviewer for this task"
20
+ * "GPT as reviewer: please review this code"
21
+ *
22
+ * The parser is forgiving — these all work:
23
+ * [reviewer: GLM] → uses GLM_MODEL from env
24
+ * [reviewer: gpt] → uses PLANNER_MODEL from env (same family)
25
+ * [reviewer: MiniMax] → uses MINIMAX_MODEL from env
26
+ *
27
+ * Wiki: wiki/multi-agent-langchain.md
28
+ */
29
+ /** Model options derived from a resolved env prefix. */
30
+ export interface ResolvedModel {
31
+ model: string;
32
+ baseURL: string;
33
+ apiKey: string;
34
+ }
35
+ /** Result of routing a request to models. */
36
+ export interface RouteResult {
37
+ planner: ResolvedModel;
38
+ reviewer: ResolvedModel;
39
+ coder: ResolvedModel;
40
+ /** Text of the request with directive comments stripped (safe for agent prompts). */
41
+ cleanRequest: string;
42
+ }
43
+ /**
44
+ * Resolve a model family to actual env-backed model options.
45
+ * family: "GLM" | "PLANNER" | "MINIMAX" (case-insensitive input)
46
+ */
47
+ export declare function resolveModel(family: string): ResolvedModel;
48
+ /**
49
+ * Route a request prompt to the correct model configuration.
50
+ *
51
+ * Algorithm:
52
+ * 1. Extract [role: family] directives from the prompt
53
+ * 2. Resolve each family to actual env-backed model options
54
+ * 3. Apply defaults (planner→PLANNER, reviewer→GLM, coder→MINIMAX)
55
+ * 4. Strip directive comments from the request before passing to agents
56
+ */
57
+ export declare function routeRequest(request: string): RouteResult;
58
+ /**
59
+ * Route a request, also scanning natural-language hints.
60
+ * Directives ([role: family]) take precedence over natural hints.
61
+ */
62
+ export declare function routeRequestSmart(request: string): RouteResult;
63
+ //# sourceMappingURL=model-router.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model-router.test.d.ts.map
@@ -0,0 +1,191 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import {
4
+ routeRequest,
5
+ routeRequestSmart,
6
+ resolveModel,
7
+ } from "./model-router.js";
8
+
9
+ const TEST_ENV = {
10
+ PLANNER_API_KEY: "pk-test-planner",
11
+ PLANNER_MODEL: "gpt-5.6",
12
+ PLANNER_BASE_URL: "https://api.openai.com/v1",
13
+ GLM_API_KEY: "pk-test-glm",
14
+ GLM_MODEL: "GLM-5.2",
15
+ GLM_BASE_URL: "https://api.z.ai/api/v1",
16
+ MINIMAX_API_KEY: "pk-test-minimax",
17
+ MINIMAX_MODEL: "MiniMax-M2",
18
+ MINIMAX_BASE_URL: "https://api.minimaxi.com/v1",
19
+ };
20
+
21
+ describe("resolveModel", () => {
22
+ const original = process.env;
23
+
24
+ beforeEach(() => {
25
+ process.env = { ...original, ...TEST_ENV };
26
+ });
27
+
28
+ afterEach(() => {
29
+ process.env = original;
30
+ });
31
+
32
+ it("resolves GLM family from env", () => {
33
+ const r = resolveModel("glm");
34
+ assert.equal(r.model, "GLM-5.2");
35
+ assert.equal(r.baseURL, "https://api.z.ai/api/v1");
36
+ assert.equal(r.apiKey, "pk-test-glm");
37
+ });
38
+
39
+ it("resolves PLANNER family from env", () => {
40
+ const r = resolveModel("planner");
41
+ assert.equal(r.model, "gpt-5.6");
42
+ assert.equal(r.apiKey, "pk-test-planner");
43
+ });
44
+
45
+ it("resolves MINIMAX family from env", () => {
46
+ const r = resolveModel("minimax");
47
+ assert.equal(r.model, "MiniMax-M2");
48
+ assert.equal(r.apiKey, "pk-test-minimax");
49
+ });
50
+
51
+ it("is case-insensitive", () => {
52
+ const r = resolveModel("GLM");
53
+ assert.equal(r.model, "GLM-5.2");
54
+ });
55
+
56
+ it("throws if API key is missing", () => {
57
+ delete process.env.GLM_API_KEY;
58
+ assert.throws(() => resolveModel("glm"), /GLM_API_KEY is not set/);
59
+ });
60
+
61
+ it("throws if MODEL is missing", () => {
62
+ delete process.env.GLM_MODEL;
63
+ assert.throws(() => resolveModel("glm"), /GLM_MODEL is not set/);
64
+ });
65
+
66
+ it("throws if BASE_URL is missing", () => {
67
+ delete process.env.GLM_BASE_URL;
68
+ assert.throws(() => resolveModel("glm"), /GLM_BASE_URL is not set/);
69
+ });
70
+ });
71
+
72
+ describe("routeRequest", () => {
73
+ const original = process.env;
74
+
75
+ beforeEach(() => {
76
+ process.env = { ...original, ...TEST_ENV };
77
+ });
78
+
79
+ afterEach(() => {
80
+ process.env = original;
81
+ });
82
+
83
+ it("default: planner→PLANNER, reviewer→GLM, coder→MINIMAX", () => {
84
+ const r = routeRequest("Implement a rate limiter");
85
+ assert.equal(r.planner.model, "gpt-5.6");
86
+ assert.equal(r.reviewer.model, "GLM-5.2");
87
+ assert.equal(r.coder.model, "MiniMax-M2");
88
+ assert.equal(r.cleanRequest, "Implement a rate limiter");
89
+ });
90
+
91
+ it("strips [role: family] directives from cleanRequest", () => {
92
+ const r = routeRequest(
93
+ "Fix the bug [planner: gpt] [reviewer: GLM] [coder: MiniMax]",
94
+ );
95
+ assert.equal(r.cleanRequest, "Fix the bug");
96
+ });
97
+
98
+ it("override reviewer with GPT directive → PLANNER env", () => {
99
+ const r = routeRequest("Implement feature X [reviewer: gpt]");
100
+ assert.equal(r.reviewer.model, "gpt-5.6");
101
+ assert.equal(r.cleanRequest, "Implement feature X");
102
+ });
103
+
104
+ it("override planner with GLM directive", () => {
105
+ const r = routeRequest("Plan this task [planner: glm]");
106
+ assert.equal(r.planner.model, "GLM-5.2");
107
+ });
108
+
109
+ it("override coder with GPT directive → PLANNER env", () => {
110
+ const r = routeRequest("Write the code [coder: gpt]");
111
+ assert.equal(r.coder.model, "gpt-5.6");
112
+ });
113
+
114
+ it("multiple directives together", () => {
115
+ const r = routeRequest(
116
+ "Implement X [planner: gpt] [reviewer: GLM] [coder: MiniMax]",
117
+ );
118
+ assert.equal(r.planner.model, "gpt-5.6");
119
+ assert.equal(r.reviewer.model, "GLM-5.2");
120
+ assert.equal(r.coder.model, "MiniMax-M2");
121
+ });
122
+
123
+ it("partial directive — only reviewer overridden", () => {
124
+ const r = routeRequest("Do it [reviewer: gpt]");
125
+ assert.equal(r.reviewer.model, "gpt-5.6");
126
+ assert.equal(r.planner.model, "gpt-5.6");
127
+ assert.equal(r.coder.model, "MiniMax-M2");
128
+ });
129
+
130
+ it("unknown family throws — must be a recognised family name", () => {
131
+ // FAMILY_MAP only knows: gpt, glm, minimax
132
+ // unknown families are passed through as-is, causing readEnv to throw
133
+ assert.throws(
134
+ () => routeRequest("Do it [reviewer: unknown-provider]"),
135
+ /UNKNOWN-PROVIDER_API_KEY is not set/,
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("routeRequestSmart — natural language hints", () => {
141
+ const original = process.env;
142
+
143
+ beforeEach(() => {
144
+ process.env = { ...original, ...TEST_ENV };
145
+ });
146
+
147
+ afterEach(() => {
148
+ process.env = original;
149
+ });
150
+
151
+ it("'use GLM as master reviewer' → reviewer→GLM", () => {
152
+ const r = routeRequestSmart(
153
+ "Implement a rate limiter. Use GLM as master reviewer.",
154
+ );
155
+ assert.equal(r.reviewer.model, "GLM-5.2");
156
+ });
157
+
158
+ it("'use GPT as reviewer' → reviewer→PLANNER", () => {
159
+ const r = routeRequestSmart("Implement a rate limiter. Use GPT as reviewer.");
160
+ assert.equal(r.reviewer.model, "gpt-5.6");
161
+ });
162
+
163
+ it("'[reviewer: GLM]' directive beats natural hint", () => {
164
+ const r = routeRequestSmart(
165
+ "Implement X. Use GPT as master reviewer. [reviewer: GLM]",
166
+ );
167
+ // directive wins
168
+ assert.equal(r.reviewer.model, "GLM-5.2");
169
+ assert.equal(r.cleanRequest, "Implement X. Use GPT as master reviewer.");
170
+ });
171
+
172
+ it("'GPT as planner' → planner→PLANNER", () => {
173
+ const r = routeRequestSmart("Plan the implementation. GPT as planner.");
174
+ assert.equal(r.planner.model, "gpt-5.6");
175
+ });
176
+
177
+ it("strips directive comments but keeps natural hints", () => {
178
+ const r = routeRequestSmart(
179
+ "Fix the bug. Use GLM as master reviewer. [reviewer: gpt]",
180
+ );
181
+ assert.equal(r.cleanRequest, "Fix the bug. Use GLM as master reviewer.");
182
+ assert.equal(r.reviewer.model, "gpt-5.6"); // directive wins
183
+ });
184
+
185
+ it("defaults all three when no hints or directives", () => {
186
+ const r = routeRequestSmart("Just do it.");
187
+ assert.equal(r.planner.model, "gpt-5.6");
188
+ assert.equal(r.reviewer.model, "GLM-5.2");
189
+ assert.equal(r.coder.model, "MiniMax-M2");
190
+ });
191
+ });