pi-harness-runtime 1.1.150 → 1.1.152

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 (158) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/harness/key-loader.ts +84 -0
  3. package/harness/langchain/agents.ts +65 -15
  4. package/harness/langchain/daemon.ts +58 -30
  5. package/harness/langchain/graph.ts +55 -7
  6. package/harness/langchain/jev-logger.ts +274 -0
  7. package/harness/langchain/run.ts +64 -161
  8. package/harness/langchain/surge.test.ts +42 -0
  9. package/harness/langchain/surge.ts +22 -1
  10. package/index.ts +24 -0
  11. package/package.json +1 -1
  12. package/packages/a2a-adapter/package.json +1 -1
  13. package/packages/agent-policy/dist/extension.d.ts +19 -0
  14. package/packages/agent-policy/dist/extension.d.ts.map +1 -0
  15. package/packages/agent-policy/dist/extension.js +322 -0
  16. package/packages/agent-policy/dist/extension.js.map +1 -0
  17. package/packages/agent-policy/dist/index.d.ts +25 -0
  18. package/packages/agent-policy/dist/index.d.ts.map +1 -0
  19. package/packages/agent-policy/dist/index.js +27 -0
  20. package/packages/agent-policy/dist/index.js.map +1 -0
  21. package/packages/agent-policy/dist/policy-loader.d.ts +90 -0
  22. package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -0
  23. package/packages/agent-policy/dist/policy-loader.js +331 -0
  24. package/packages/agent-policy/dist/policy-loader.js.map +1 -0
  25. package/packages/agent-policy/dist/policy-renderer.d.ts +42 -0
  26. package/packages/agent-policy/dist/policy-renderer.d.ts.map +1 -0
  27. package/packages/agent-policy/dist/policy-renderer.js +295 -0
  28. package/packages/agent-policy/dist/policy-renderer.js.map +1 -0
  29. package/packages/agent-policy/dist/policy-store.d.ts +99 -0
  30. package/packages/agent-policy/dist/policy-store.d.ts.map +1 -0
  31. package/packages/agent-policy/dist/policy-store.js +244 -0
  32. package/packages/agent-policy/dist/policy-store.js.map +1 -0
  33. package/packages/agent-policy/dist/types.d.ts +237 -0
  34. package/packages/agent-policy/dist/types.d.ts.map +1 -0
  35. package/packages/agent-policy/dist/types.js +130 -0
  36. package/packages/agent-policy/dist/types.js.map +1 -0
  37. package/packages/agent-policy/package.json +45 -0
  38. package/packages/agent-policy/src/extension.ts +413 -0
  39. package/packages/agent-policy/src/index.ts +52 -0
  40. package/packages/agent-policy/src/policy-loader.ts +398 -0
  41. package/packages/agent-policy/src/policy-renderer.ts +362 -0
  42. package/packages/agent-policy/src/policy-store.ts +319 -0
  43. package/packages/agent-policy/src/types.ts +385 -0
  44. package/packages/agent-policy/test/policy.test.ts +383 -0
  45. package/packages/architecture-generator/package.json +1 -1
  46. package/packages/auth/package.json +1 -1
  47. package/packages/autonomous-refactor/package.json +1 -1
  48. package/packages/autonomous-runtime/package.json +1 -1
  49. package/packages/cache-strategy/package.json +1 -1
  50. package/packages/capability-registry/package.json +1 -1
  51. package/packages/checkpoint/package.json +1 -1
  52. package/packages/cli-plugin-sdk/package.json +1 -1
  53. package/packages/clipboard/package.json +1 -1
  54. package/packages/clipboard-plugin/package.json +1 -1
  55. package/packages/code-generation/package.json +1 -1
  56. package/packages/code-review/package.json +1 -1
  57. package/packages/codex-adapter/package.json +1 -1
  58. package/packages/codex-watcher/dist/src/parser.d.ts.map +1 -1
  59. package/packages/codex-watcher/dist/src/parser.js +35 -0
  60. package/packages/codex-watcher/dist/src/parser.js.map +1 -1
  61. package/packages/codex-watcher/package.json +1 -1
  62. package/packages/codex-watcher/src/parser.ts +41 -0
  63. package/packages/config-capture/package.json +1 -1
  64. package/packages/context-compiler/package.json +1 -1
  65. package/packages/context-discovery/package.json +1 -1
  66. package/packages/context-manager/package.json +1 -1
  67. package/packages/cookie-sanitizer/package.json +1 -1
  68. package/packages/cost-optimizer/package.json +1 -1
  69. package/packages/dependency-analyzer/package.json +1 -1
  70. package/packages/django-plugin/package.json +1 -1
  71. package/packages/doc-generator/package.json +1 -1
  72. package/packages/evaluation-engine/package.json +1 -1
  73. package/packages/evaluation-runner/package.json +1 -1
  74. package/packages/event-bus/package.json +1 -1
  75. package/packages/event-store/package.json +1 -1
  76. package/packages/experience-replay/package.json +1 -1
  77. package/packages/feedback-collector/package.json +1 -1
  78. package/packages/file-copy-helper/package.json +1 -1
  79. package/packages/framework-detector/package.json +1 -1
  80. package/packages/framework-plugin-sdk/package.json +1 -1
  81. package/packages/frappe-plugin/package.json +1 -1
  82. package/packages/generic-web-plugin/package.json +1 -1
  83. package/packages/health-monitor/package.json +1 -1
  84. package/packages/intent-analyzer/package.json +1 -1
  85. package/packages/jev-judge/package.json +1 -1
  86. package/packages/knowledge-graph/package.json +1 -1
  87. package/packages/knowledge-retrieval/package.json +1 -1
  88. package/packages/laravel-plugin/package.json +1 -1
  89. package/packages/learning-engine/package.json +1 -1
  90. package/packages/mcp-adapter/package.json +1 -1
  91. package/packages/memory-engine/package.json +1 -1
  92. package/packages/milestone-manager/package.json +1 -1
  93. package/packages/model-registry/package.json +1 -1
  94. package/packages/nextjs-plugin/package.json +1 -1
  95. package/packages/notification/dist/adapters/telegram-adapter.d.ts +5 -0
  96. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  97. package/packages/notification/dist/adapters/telegram-adapter.js +54 -0
  98. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  99. package/packages/notification/dist/notification-center.d.ts +13 -0
  100. package/packages/notification/dist/notification-center.d.ts.map +1 -1
  101. package/packages/notification/dist/notification-center.js +36 -0
  102. package/packages/notification/dist/notification-center.js.map +1 -1
  103. package/packages/notification/package.json +1 -1
  104. package/packages/notification/src/adapters/telegram-adapter.ts +71 -0
  105. package/packages/notification/src/notification-center.ts +53 -0
  106. package/packages/observability/package.json +1 -1
  107. package/packages/okf-indexer/package.json +1 -1
  108. package/packages/paperclip-integration/package.json +1 -1
  109. package/packages/performance-optimizer/package.json +1 -1
  110. package/packages/privilege-broker/package.json +1 -1
  111. package/packages/project-analyzer/package.json +1 -1
  112. package/packages/project-bootstrap/package.json +1 -1
  113. package/packages/projection-engine/package.json +1 -1
  114. package/packages/prompt-compiler/package.json +1 -1
  115. package/packages/prompt-versioning/package.json +1 -1
  116. package/packages/provider-adapter-sdk/package.json +1 -1
  117. package/packages/provider-router/package.json +1 -1
  118. package/packages/provider-selector/package.json +1 -1
  119. package/packages/providers/package.json +1 -1
  120. package/packages/qdrant-skills/package.json +1 -1
  121. package/packages/quota-manager/package.json +1 -1
  122. package/packages/rate-limiter/package.json +1 -1
  123. package/packages/react-vite-plugin/package.json +1 -1
  124. package/packages/release-manager/package.json +1 -1
  125. package/packages/requirement-compiler/package.json +1 -1
  126. package/packages/runtime/package.json +1 -1
  127. package/packages/scheduler/package.json +1 -1
  128. package/packages/scheduler-adapter/package.json +1 -1
  129. package/packages/session/package.json +1 -1
  130. package/packages/session-api/package.json +1 -1
  131. package/packages/session-export/package.json +1 -1
  132. package/packages/shared-context/package.json +1 -1
  133. package/packages/skill-mcp-client/package.json +1 -1
  134. package/packages/skill-registry/package.json +1 -1
  135. package/packages/skills/package.json +1 -1
  136. package/packages/sprint-planner/package.json +1 -1
  137. package/packages/subscription-engine/package.json +1 -1
  138. package/packages/task-compiler/package.json +1 -1
  139. package/packages/tencentdb-memory/package.json +1 -1
  140. package/packages/tencentdb-sync/package.json +1 -1
  141. package/packages/test-data-generator/package.json +1 -1
  142. package/packages/test-generator/package.json +1 -1
  143. package/packages/todo-bd-sync/package.json +1 -1
  144. package/packages/token-estimation/package.json +1 -1
  145. package/packages/token-optimizer/package.json +1 -1
  146. package/packages/trajectory/package.json +1 -1
  147. package/packages/tui/package.json +1 -1
  148. package/packages/types/package.json +1 -1
  149. package/packages/workflow-events/package.json +1 -1
  150. package/packages/workspace-scanner/package.json +1 -1
  151. package/packages/worktree/package.json +1 -1
  152. package/packages/write-review/package.json +1 -1
  153. package/proactive-compact.ts +2 -2
  154. package/scripts/debug-codex-integration.ts +203 -0
  155. package/scripts/sync-keys-from-pi.ts +188 -0
  156. package/scripts/test-codex-session.ts +23 -0
  157. package/scripts/test-openai.ts +14 -0
  158. package/scripts/test-telegram.ts +34 -0
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Jev Logger - Exclusive logging for Jev's decisions
3
+ *
4
+ * Logs all Jev decisions:
5
+ * - Provider selection (minimax vs gpt)
6
+ * - User input decisions
7
+ * - Escalation triggers
8
+ * - Reasoning
9
+ *
10
+ * Sends to both file and Telegram for real-time monitoring.
11
+ */
12
+
13
+ import { mkdirSync, appendFileSync, existsSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { homedir } from "node:os";
16
+
17
+ export interface JevDecision {
18
+ timestamp: string;
19
+ request: string;
20
+ provider: "minimax" | "gpt";
21
+ reason: string;
22
+ userInput: {
23
+ type: "yesNo" | "open" | null;
24
+ question?: string;
25
+ } | null;
26
+ escalate: boolean;
27
+ escalateReason?: string;
28
+ }
29
+
30
+ export interface JevLogEntry extends JevDecision {
31
+ raw?: string; // Raw planner output for debugging
32
+ }
33
+
34
+ const LOG_DIR = join(homedir(), ".pi-harness-runtime", "logs");
35
+ const LOG_FILE = join(LOG_DIR, "jev.log");
36
+
37
+ // Telegram config (loaded from environment)
38
+ let TELEGRAM_BOT_TOKEN: string | undefined;
39
+ let TELEGRAM_CHAT_ID: string | undefined;
40
+ let telegramQueue: string[] = [];
41
+ let telegramSending = false;
42
+
43
+ export function configureTelegram(botToken?: string, chatId?: string): void {
44
+ TELEGRAM_BOT_TOKEN = botToken ?? process.env.TELEGRAM_BOT_TOKEN;
45
+ TELEGRAM_CHAT_ID = chatId ?? process.env.TELEGRAM_CHAT_ID;
46
+ }
47
+
48
+ function ensureLogDir(): void {
49
+ if (!existsSync(LOG_DIR)) {
50
+ mkdirSync(LOG_DIR, { recursive: true });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Format decision for file log (detailed, multi-line)
56
+ */
57
+ function formatFileLog(entry: JevLogEntry): string {
58
+ const lines = [
59
+ `═══════════════════════════════════════════════════════════════`,
60
+ `[JEV] ${entry.timestamp}`,
61
+ `═══════════════════════════════════════════════════════════════`,
62
+ ``,
63
+ `## Request`,
64
+ `${entry.request}`,
65
+ ``,
66
+ `## Jev Provider Decision`,
67
+ `Provider: ${entry.provider.toUpperCase()}`,
68
+ `Reason: ${entry.reason}`,
69
+ ``,
70
+ `## Jev User Input Decision`,
71
+ `Type: ${entry.userInput?.type ?? "none (proceed automatically)"}`,
72
+ entry.userInput?.question ? `Question: ${entry.userInput.question}` : ``,
73
+ ``,
74
+ `## Escalation`,
75
+ `Escalate: ${entry.escalate ? "YES → Switching to GPT" : "No"}`,
76
+ entry.escalateReason ? `Reason: ${entry.escalateReason}` : ``,
77
+ ``,
78
+ ];
79
+
80
+ if (entry.raw) {
81
+ lines.push(`## Raw Planner Output`);
82
+ lines.push(`\`\`\`\n${entry.raw.slice(0, 500)}\n...\`\`\``); // Truncate for file
83
+ lines.push(``);
84
+ }
85
+
86
+ lines.push(`═══════════════════════════════════════════════════════════════`);
87
+ lines.push(``);
88
+
89
+ return lines.join("\n");
90
+ }
91
+
92
+ /**
93
+ * Format decision for Telegram (compact, single message)
94
+ */
95
+ function formatTelegramMessage(entry: JevLogEntry): string {
96
+ const providerIcon = entry.provider === "gpt" ? "🤖" : "⚡";
97
+ const escalateIcon = entry.escalate ? " ↑ ESCALATE" : "";
98
+
99
+ const parts = [
100
+ `${providerIcon} *JEV DECISION*`,
101
+ `\n*Provider:* ${entry.provider.toUpperCase()}${escalateIcon}`,
102
+ `\n*Reason:* ${entry.reason}`,
103
+ ];
104
+
105
+ if (entry.userInput?.question) {
106
+ const inputIcon = entry.userInput.type === "yesNo" ? "❓" : "💬";
107
+ parts.push(`\n${inputIcon} *USER INPUT:* ${entry.userInput.question}`);
108
+ }
109
+
110
+ // Truncate request for Telegram (usually too long)
111
+ const shortRequest = entry.request.length > 100
112
+ ? entry.request.slice(0, 100) + "..."
113
+ : entry.request;
114
+ parts.push(`\n\n📋 *Request:* ${shortRequest}`);
115
+
116
+ return parts.join("");
117
+ }
118
+
119
+ /**
120
+ * Send message to Telegram (async, queued)
121
+ */
122
+ async function sendToTelegram(message: string): Promise<void> {
123
+ // Always try to load from env first
124
+ TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? TELEGRAM_BOT_TOKEN;
125
+ TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? TELEGRAM_CHAT_ID;
126
+
127
+ if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
128
+ console.log("[JEV] Telegram not configured - skipping");
129
+ return;
130
+ }
131
+
132
+ console.log(`[JEV] Telegram sending: ${TELEGRAM_BOT_TOKEN?.slice(0, 10)}... to ${TELEGRAM_CHAT_ID}`);
133
+ telegramQueue.push(message);
134
+
135
+ // Process queue sequentially
136
+ if (telegramSending) {
137
+ console.log("[JEV] Telegram already sending, queued");
138
+ return;
139
+ }
140
+ telegramSending = true;
141
+
142
+ while (telegramQueue.length > 0) {
143
+ const msg = telegramQueue.shift()!;
144
+ try {
145
+ const response = await fetch(
146
+ `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
147
+ {
148
+ method: "POST",
149
+ headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({
151
+ chat_id: TELEGRAM_CHAT_ID,
152
+ text: msg,
153
+ parse_mode: "Markdown",
154
+ }),
155
+ },
156
+ );
157
+
158
+ if (!response.ok) {
159
+ console.error(`[JEV] Telegram send failed: ${response.status}`);
160
+ } else {
161
+ console.log("[JEV] Telegram sent successfully");
162
+ }
163
+ } catch (err) {
164
+ console.error(`[JEV] Telegram error:`, err);
165
+ }
166
+ // Rate limit: 1 message per second
167
+ await new Promise((resolve) => setTimeout(resolve, 1000));
168
+ }
169
+
170
+ telegramSending = false;
171
+ }
172
+
173
+ export async function logJevDecision(decision: JevLogEntry): Promise<void> {
174
+ ensureLogDir();
175
+
176
+ // Always log to file (sync)
177
+ const fileLog = formatFileLog(decision);
178
+ appendFileSync(LOG_FILE, fileLog);
179
+ console.log(fileLog);
180
+
181
+ // Send to Telegram (async, non-blocking for main flow)
182
+ const telegramMsg = formatTelegramMessage(decision);
183
+ // Fire-and-forget: don't await, let it run in background
184
+ sendToTelegram(telegramMsg).catch((err) => {
185
+ console.error("[JEV] Failed to send Telegram notification:", err);
186
+ });
187
+ // Also await briefly for testing, then let process exit handle it
188
+ await new Promise((resolve) => setTimeout(resolve, 100));
189
+ }
190
+
191
+ /**
192
+ * Parse Jev's decision from planner output
193
+ * Looks for [JEVO] and [USER_INPUT] markers
194
+ */
195
+ export function parseJevDecision(planOutput: string): {
196
+ provider: "minimax" | "gpt";
197
+ reason: string;
198
+ userInput: JevDecision["userInput"];
199
+ } {
200
+ const result: {
201
+ provider: "minimax" | "gpt";
202
+ reason: string;
203
+ userInput: JevDecision["userInput"];
204
+ } = {
205
+ provider: "minimax",
206
+ reason: "default (no explicit decision)",
207
+ userInput: null,
208
+ };
209
+
210
+ // Parse [JEVO] block
211
+ const jevoMatch = planOutput.match(/\[JEVO\]\s*\{[\s\S]*?"provider":\s*"(minimax|gpt)"[\s\S]*?"reason":\s*"([^"]*)"[\s\S]*?\}/);
212
+ if (jevoMatch) {
213
+ result.provider = jevoMatch[1] as "minimax" | "gpt";
214
+ result.reason = jevoMatch[2] || "explicit provider decision";
215
+ }
216
+
217
+ // Also check for [use-gpt] directive in the raw text
218
+ if (/\[use-gpt(?:-only)?\]/i.test(planOutput)) {
219
+ result.provider = "gpt";
220
+ result.reason = "user directive: [use-gpt]";
221
+ }
222
+
223
+ // Parse [USER_INPUT] block
224
+ const userInputMatch = planOutput.match(
225
+ /\[USER_INPUT\]\s*(\{[\s\S]*?\})/,
226
+ );
227
+ if (userInputMatch) {
228
+ try {
229
+ const parsed = JSON.parse(userInputMatch[1]);
230
+ if (parsed.type === "yesNo" || parsed.type === "open") {
231
+ result.userInput = {
232
+ type: parsed.type,
233
+ question: parsed.question,
234
+ };
235
+ }
236
+ } catch {
237
+ // Ignore parse errors
238
+ }
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * Check if review indicates escalation needed
246
+ */
247
+ export function shouldEscalate(reviewComments: string[]): {
248
+ should: boolean;
249
+ reason: string;
250
+ } {
251
+ const escalationKeywords = [
252
+ "blocked",
253
+ "stuck",
254
+ "cannot proceed",
255
+ "complex",
256
+ "race condition",
257
+ "security issue",
258
+ "architecture decision needed",
259
+ ];
260
+
261
+ for (const comment of reviewComments) {
262
+ const lowerComment = comment.toLowerCase();
263
+ for (const keyword of escalationKeywords) {
264
+ if (lowerComment.includes(keyword)) {
265
+ return {
266
+ should: true,
267
+ reason: `Review flagged: "${keyword}"`,
268
+ };
269
+ }
270
+ }
271
+ }
272
+
273
+ return { should: false, reason: "" };
274
+ }
@@ -2,14 +2,17 @@
2
2
  * CLI runner for the LangChain write-review loop.
3
3
  *
4
4
  * Usage:
5
- * bun harness/langchain/run.ts --mode graph --request "implement X" [--max-iterations 3]
6
- * bun harness/langchain/run.ts --mode graph --request "implement X" --dry-run
7
- * bun harness/langchain/run.ts --mode supervisor --request "implement X"
5
+ * # Run a single task
6
+ * bun run harness/langchain/run.ts --request "implement X"
8
7
  *
9
- * --dry-run uses deterministic stubs — no API keys needed. Use it to verify
10
- * the whole loop machinery (plan → write → review → fix → approve).
8
+ * # Run with dry-run (no API calls)
9
+ * bun run harness/langchain/run.ts --request "implement X" --dry-run
11
10
  *
12
- * Wiki: wiki/multi-agent-langchain.md
11
+ * # Run with max iterations
12
+ * bun run harness/langchain/run.ts --request "implement X" --max-iterations 5
13
+ *
14
+ * # Run as daemon (with watchers - configure sources in code)
15
+ * bun run harness/langchain/run.ts --daemon
13
16
  */
14
17
 
15
18
  import { randomUUID } from "node:crypto";
@@ -17,17 +20,16 @@ import {
17
20
  buildDryRunDeps,
18
21
  buildRealLoopDeps,
19
22
  buildWriteReviewLoop,
20
- type LoopState,
21
23
  } from "./graph.js";
22
-
24
+ import { loadKeys } from "../key-loader.js";
23
25
  interface CliArgs {
24
26
  mode: "graph" | "supervisor";
25
27
  request: string;
26
28
  maxIterations: number;
27
29
  dryRun: boolean;
28
30
  daemon: boolean;
29
- /** If true, remaining args are passed to the cron subcommand handler. */
30
- cronArgs?: string[];
31
+ telegramBotToken?: string;
32
+ telegramChatId?: string;
31
33
  }
32
34
 
33
35
  function parseArgs(argv: string[]): CliArgs {
@@ -37,183 +39,84 @@ function parseArgs(argv: string[]): CliArgs {
37
39
  maxIterations: 3,
38
40
  dryRun: false,
39
41
  daemon: false,
42
+ telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
43
+ telegramChatId: process.env.TELEGRAM_CHAT_ID,
40
44
  };
41
- // Normalize space-separated flags (--mode graph) into --mode=graph form
42
- const normalized: string[] = [];
43
- for (let i = 0; i < argv.length; i++) {
44
- const arg = argv[i] as string;
45
- const spaced =
46
- (arg === "--mode" || arg === "--request" || arg === "--max-iterations") &&
47
- argv[i + 1] !== undefined &&
48
- !(argv[i + 1] as string).startsWith("--");
49
- if (spaced) {
50
- normalized.push(`${arg}=${argv[i + 1]}`);
51
- i++;
52
- } else {
53
- normalized.push(arg);
54
- }
55
- }
56
- // Detect "cron" subcommand before processing other flags
57
- const cronIndex = normalized.indexOf("cron");
58
- if (cronIndex !== -1) {
59
- args.cronArgs = normalized.slice(cronIndex + 1);
60
- return args;
61
- }
62
45
 
63
- for (const arg of normalized) {
64
- if (arg.startsWith("--mode=")) {
65
- const mode = arg.slice("--mode=".length);
66
- if (mode !== "graph" && mode !== "supervisor") {
67
- throw new Error(`Unknown mode: ${mode} (expected graph|supervisor)`);
68
- }
69
- args.mode = mode;
70
- } else if (arg.startsWith("--request=")) {
71
- args.request = arg.slice("--request=".length);
72
- } else if (arg.startsWith("--max-iterations=")) {
73
- const n = Number.parseInt(arg.slice("--max-iterations=".length), 10);
74
- if (Number.isNaN(n) || n < 1 || n > 20) {
75
- throw new Error("--max-iterations must be 1-20");
76
- }
77
- args.maxIterations = n;
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const arg = argv[i];
48
+ if (arg === "--request" || arg === "-r") {
49
+ args.request = argv[++i] ?? "";
78
50
  } else if (arg === "--dry-run") {
79
51
  args.dryRun = true;
80
52
  } else if (arg === "--daemon") {
81
53
  args.daemon = true;
82
- } else if (arg === "--help" || arg === "-h") {
83
- console.log(
84
- [
85
- "Usage: bun harness/langchain/run.ts [options]",
86
- "",
87
- "Options:",
88
- " --mode=graph|supervisor Loop style (default: graph)",
89
- ' --request="..." The feature request',
90
- " --max-iterations=N Max write-review rounds (default: 3)",
91
- " --dry-run Deterministic stubs, no API calls",
92
- " --daemon Start as a long-running daemon (auto-trigger loop)",
93
- ].join("\n"),
94
- );
95
- process.exit(0);
96
- } else {
97
- // Bare positional argument = request
98
- if (!args.request) args.request = arg;
54
+ } else if (arg === "--max-iterations") {
55
+ args.maxIterations = parseInt(argv[++i] ?? "3", 10);
56
+ } else if (arg === "--mode") {
57
+ args.mode = argv[++i] as "graph" | "supervisor";
99
58
  }
100
59
  }
101
- if (!args.request && !args.daemon && !args.cronArgs) {
102
- throw new Error(
103
- 'A request is required: --request="..." or a bare string (or use --daemon or --cron to start the watcher)',
104
- );
105
- }
106
- return args;
107
- }
108
60
 
109
- function printStep(step: string, state: LoopState): void {
110
- const labels: Record<string, string> = {
111
- plan: "🧭 GPT planner",
112
- write: "✍️ MiniMax coder",
113
- review: "🔍 GPT reviewer",
114
- finish: "🏁 finish",
115
- };
116
- const iter = state.iteration > 0 ? ` (iteration ${state.iteration})` : "";
117
- console.log(` ${labels[step] ?? step}${iter}`);
61
+ return args;
118
62
  }
119
63
 
120
- async function runGraphLoop(args: CliArgs): Promise<void> {
121
- console.log(
122
- `\n━━━ Write-Review Loop (graph mode)${args.dryRun ? " — DRY RUN" : ""} ━━━`,
123
- );
124
- console.log(`Request: ${args.request}\n`);
64
+ async function runGraphMode(args: CliArgs): Promise<void> {
65
+ const loopId = `loop-${randomUUID().slice(0, 8)}`;
66
+ const log = (msg: string) => console.log(`[${loopId}] ${msg}`);
125
67
 
126
- const deps = args.dryRun
127
- ? buildDryRunDeps({ maxIterations: args.maxIterations, onStep: printStep })
128
- : await buildRealLoopDeps({
68
+ // Build deps
69
+ const deps = await (args.dryRun
70
+ ? buildDryRunDeps({ maxIterations: args.maxIterations })
71
+ : buildRealLoopDeps({
129
72
  maxIterations: args.maxIterations,
130
- onStep: printStep,
131
- });
73
+ request: args.request,
74
+ }));
132
75
 
76
+ // Create loop
133
77
  const loop = buildWriteReviewLoop(deps);
134
- const threadId = `loop-${randomUUID().slice(0, 8)}`;
135
-
136
- const finalState = await loop.invoke(
137
- { request: args.request },
138
- { configurable: { thread_id: threadId } },
139
- );
140
78
 
141
- console.log("\n─── Step log ───");
142
- for (const line of finalState.log) console.log(` ${line}`);
79
+ // Run
80
+ log(`Starting write-review loop (maxIterations=${args.maxIterations})`);
143
81
 
144
- console.log("\n─── Verdict ───");
145
- const review = finalState.review;
146
- if (review) {
147
- console.log(` ${review.verdict.toUpperCase()} — ${review.summary}`);
148
- if (review.comments.length > 0) {
149
- console.log(" Open comments:");
150
- for (const c of review.comments) {
151
- const file = c.file ? ` (${c.file})` : "";
152
- console.log(` - [${c.severity ?? "n/a"}] ${c.comment}${file}`);
153
- }
154
- }
155
- } else {
156
- console.log(" (no review produced)");
157
- }
158
- }
159
-
160
- async function runSupervisor(args: CliArgs): Promise<void> {
161
- if (args.dryRun) {
162
- throw new Error(
163
- "--dry-run is only supported for --mode=graph (supervisor needs real models)",
82
+ try {
83
+ const result = await loop.invoke(
84
+ { request: args.request },
85
+ { configurable: { thread_id: loopId }, recursionLimit: 50 },
164
86
  );
87
+ log(`Loop completed! Final state: ${JSON.stringify(result, null, 2)}`);
88
+ } catch (err) {
89
+ console.error(`[${loopId}] Error:`, err);
90
+ process.exit(1);
165
91
  }
166
- console.log("\n━━━ Write-Review Loop (supervisor mode) ━━━");
167
- console.log(`Request: ${args.request}\n`);
168
-
169
- const { createSupervisor, lastMessage } = await import("./agents.js");
170
- const supervisor = createSupervisor();
171
- const result = await supervisor.invoke({
172
- messages: [{ role: "user", content: args.request }],
173
- });
174
- console.log("\n─── Supervisor output ───");
175
- console.log(lastMessage(result));
176
92
  }
177
93
 
178
- async function runDaemon(args: CliArgs): Promise<void> {
179
- const { LoopDaemon } = await import("./daemon.js");
180
- const daemon = new LoopDaemon({
181
- maxIterations: args.maxIterations,
182
- dryRun: args.dryRun,
183
- sources: ["inbox", "bus", "codex"],
184
- });
185
-
186
- // Graceful shutdown on SIGTERM / SIGINT
187
- const shutdown = () => {
188
- daemon.stop();
189
- process.exit(0);
190
- };
191
- process.on("SIGTERM", shutdown);
192
- process.on("SIGINT", shutdown);
193
-
194
- daemon.start();
195
-
196
- // Keep the process alive
197
- await new Promise(() => {});
94
+ async function runDaemonMode(_args: CliArgs): Promise<void> {
95
+ console.log("Daemon mode - use --request to run a task or configure watchers");
96
+ process.exit(0);
198
97
  }
199
98
 
200
99
  async function main(): Promise<void> {
201
100
  const args = parseArgs(process.argv.slice(2));
202
- if (args.cronArgs !== undefined) {
203
- const { runCron } = await import("./cron-cli.js");
204
- await runCron(args.cronArgs);
205
- } else if (args.daemon) {
206
- await runDaemon(args);
207
- } else if (args.mode === "supervisor") {
208
- await runSupervisor(args);
101
+
102
+ // Load API keys
103
+ loadKeys();
104
+
105
+ if (!args.request && !args.daemon) {
106
+ console.error("Usage: bun run harness/langchain/run.ts --request 'task description'");
107
+ console.error("Options:");
108
+ console.error(" --request, -r Task description");
109
+ console.error(" --dry-run Run without API calls");
110
+ console.error(" --max-iterations N Max iterations (default: 3)");
111
+ console.error(" --daemon Run in daemon mode");
112
+ process.exit(1);
113
+ }
114
+
115
+ if (args.daemon) {
116
+ await runDaemonMode(args);
209
117
  } else {
210
- await runGraphLoop(args);
118
+ await runGraphMode(args);
211
119
  }
212
120
  }
213
121
 
214
- main().catch((err: unknown) => {
215
- console.error(
216
- `[run] Error: ${err instanceof Error ? err.message : String(err)}`,
217
- );
218
- process.exit(1);
219
- });
122
+ main().catch(console.error);
@@ -76,6 +76,48 @@ describe("T1 — classifySurge", () => {
76
76
  assert.equal(classifySurge(new Error("ECONNREFUSED")), null);
77
77
  assert.equal(classifySurge("file not found"), null);
78
78
  });
79
+
80
+ it("classifies 'Request timed out.'", () => {
81
+ const sig = classifySurge(new Error("Request timed out."));
82
+ assert.ok(sig, "should classify timeout");
83
+ assert.equal(sig?.explicit, false); // no stated delay → 2 min default
84
+ assert.equal(sig?.retryAfterMs, 120_000);
85
+ });
86
+
87
+ it("classifies 'timed out' without 'request' prefix", () => {
88
+ const sig = classifySurge(new Error("timed out"));
89
+ assert.ok(sig, "should classify");
90
+ assert.equal(sig?.retryAfterMs, 120_000);
91
+ });
92
+
93
+ it("parses 'timed out after Xms'", () => {
94
+ const sig = classifySurge(new Error("timed out after 30000ms"));
95
+ assert.ok(sig);
96
+ assert.equal(sig?.retryAfterMs, 30_000);
97
+ assert.equal(sig?.explicit, true);
98
+ });
99
+
100
+ it("parses 'Request timed out after X seconds'", () => {
101
+ const sig = classifySurge(new Error("Request timed out after 60 seconds"));
102
+ assert.ok(sig);
103
+ assert.equal(sig?.retryAfterMs, 60_000);
104
+ assert.equal(sig?.explicit, true);
105
+ });
106
+
107
+ it("parses 'timed out after Xms' (milliseconds keyword)", () => {
108
+ const sig = classifySurge(new Error("command timed out after 15000 milliseconds"));
109
+ assert.ok(sig);
110
+ assert.equal(sig?.retryAfterMs, 15_000);
111
+ assert.equal(sig?.explicit, true);
112
+ });
113
+
114
+ it("'Retry failed after 3 attempts: Request timed out.'", () => {
115
+ const sig = classifySurge(
116
+ new Error("Retry failed after 3 attempts: Request timed out."),
117
+ );
118
+ assert.ok(sig, "should classify inner timeout");
119
+ assert.equal(sig?.retryAfterMs, 120_000);
120
+ });
79
121
  });
80
122
 
81
123
  // ─── T2/T3 Delay computation & escalation ────────────────────────────────────
@@ -25,11 +25,14 @@ const OVERLOADED_PATTERNS: RegExp[] = [
25
25
  /overloaded_error/i,
26
26
  /peak.?hour.*surge/i,
27
27
  /(?:overload|surge|busy)/i,
28
+ /(?:request )?timed?\s*out/i,
28
29
  ];
29
30
 
30
31
  const RECOVERY_RANGE = /recovers? within (\d+)\s*[\u2013-]\s*(\d+)\s*minutes/i;
31
32
  const SIMPLE_DELAY = /retry (?:after|in) (\d+)\s*(seconds?|secs?|s)\b/i;
32
33
  const RETRY_AFTER_MS = /retry.?after[^0-9]{0,12}(\d{4,})\s*ms/i;
34
+ const TIMEOUT_DURATION_MS = /timed?\s*out(?: after)?[^0-9]*(\d+)\s*(ms|milliseconds?)/i;
35
+ const TIMEOUT_DURATION_S = /timed?\s*out(?: after)?[^0-9]*(\d+)\s*(seconds?|secs?|s)\b/i;
33
36
 
34
37
  /**
35
38
  * Classify an unknown error as a transient provider surge (MiniMax).
@@ -83,6 +86,24 @@ export function classifySurge(err: unknown): SurgeSignal | null {
83
86
  }
84
87
  }
85
88
 
89
+ // "timed out after Xms" → use that duration
90
+ const timeoutMs = sourceText.match(TIMEOUT_DURATION_MS);
91
+ if (timeoutMs) {
92
+ const val = Number.parseInt(timeoutMs[1] as string, 10);
93
+ if (Number.isFinite(val) && val > 0) {
94
+ return { retryAfterMs: val, explicit: true, sourceText };
95
+ }
96
+ }
97
+
98
+ // "timed out after X seconds" → convert to ms
99
+ const timeoutSecs = sourceText.match(TIMEOUT_DURATION_S);
100
+ if (timeoutSecs) {
101
+ const val = Number.parseInt(timeoutSecs[1] as string, 10);
102
+ if (Number.isFinite(val) && val > 0) {
103
+ return { retryAfterMs: val * 1000, explicit: true, sourceText };
104
+ }
105
+ }
106
+
86
107
  // Overload-class error without a stated delay → conservative default
87
108
  return { retryAfterMs: 120_000, explicit: false, sourceText };
88
109
  }
@@ -188,7 +209,7 @@ export interface SurgePolicy {
188
209
  const DEFAULT_SURGE_POLICY: SurgePolicy = {
189
210
  multiplier: 2,
190
211
  minDelayMs: 30_000,
191
- maxDelayMs: 15 * 60_000,
212
+ maxDelayMs: 15 * 60 * 1000,
192
213
  jitterRatio: 0.2,
193
214
  maxAttempts: 5,
194
215
  };
package/index.ts CHANGED
@@ -648,6 +648,27 @@ async function initFileCopyHelper(pi: ExtensionAPI): Promise<void> {
648
648
  }
649
649
  }
650
650
 
651
+ // --- agent-policy: Mandatory rule delivery and mutation gating ------------------
652
+ // Lazy import - only loads when packages/agent-policy exists
653
+ // This replaces firstAgentStart-only injection with:
654
+ // - System prompt injection on EVERY agent start (not just first)
655
+ // - Policy receipt tracking per session
656
+ // - Mutation gating until policy revision is confirmed received
657
+ // - harness_rules tool for rule retrieval and acknowledgement
658
+ async function initAgentPolicy(pi: ExtensionAPI): Promise<void> {
659
+ try {
660
+ const { registerAgentPolicy } = await import("./packages/agent-policy/src/extension.js");
661
+ registerAgentPolicy(pi, {
662
+ harnessRulesPath: new URL("./AGENTS.md", import.meta.url),
663
+ requireReceiptBeforeMutation: true,
664
+ lspMode: "pi-lens",
665
+ });
666
+ logStartup("[pi-harness] Agent policy initialized");
667
+ } catch (err) {
668
+ logStartup(`[pi-harness] Agent policy init failed: ${err}`);
669
+ }
670
+ }
671
+
651
672
  // --- SSH detach interceptor: Auto-transform bare ssh ... & to detached pattern ---
652
673
  // Prevents 2000+ second hangs when SSH background commands aren't detached.
653
674
  // Intercepts every bash tool call before execution and rewrites risky SSH commands.
@@ -1173,6 +1194,9 @@ export default function (pi: ExtensionAPI) {
1173
1194
  // --- file-copy-helper: Inject cp rule when mimicking files -------------
1174
1195
  void initFileCopyHelper(pi);
1175
1196
 
1197
+ // --- agent-policy: Mandatory rule delivery and mutation gating -------------
1198
+ void initAgentPolicy(pi);
1199
+
1176
1200
  // --- ssh-detach-interceptor: Auto-fix bare ssh & → nohup pattern ---------
1177
1201
  void initSshDetachInterceptor(pi);
1178
1202