pi-harness-runtime 1.1.16 → 1.1.26
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.
- package/CHANGELOG.md +26 -0
- package/README.md +76 -18
- package/harness/e2e/auto-test-loop.ts +815 -0
- package/harness/e2e/tools-detector.test.ts +156 -0
- package/harness/e2e/tools-detector.ts +801 -0
- package/harness/glm-quota-logger.ts +20 -23
- package/harness/index.ts +42 -0
- package/harness/loop-runtime.ts +49 -13
- package/harness/skill-sync.test.ts +57 -0
- package/harness/skill-sync.ts +247 -0
- package/harness/ssh-hang-recovery.ts +280 -0
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +28 -0
- package/packages/skill-mcp-client/src/client.ts +171 -0
- package/packages/skill-mcp-client/src/errors.ts +75 -0
- package/packages/skill-mcp-client/src/index.ts +13 -0
- package/packages/skill-mcp-client/src/types.ts +123 -0
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Hang Recovery — Smart SSH wrapper that auto-recovers from hangs
|
|
3
|
+
*
|
|
4
|
+
* Problem: SSH commands can hang indefinitely (PM2 restart, sudo prompts, etc.)
|
|
5
|
+
* Solution: Timeout + process killing + retry + escalation
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* Execute SSH command
|
|
9
|
+
* → Timeout reached? → Kill stuck processes → Retry
|
|
10
|
+
* → Retry failed? → Kill PM2 daemon processes → Retry
|
|
11
|
+
* → Still failing? → Escalate to human with diagnostic
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync, exec } from "child_process";
|
|
15
|
+
import { setTimeout as setAsyncTimeout, clearTimeout } from "timers";
|
|
16
|
+
|
|
17
|
+
export interface SSHConfig {
|
|
18
|
+
host: string;
|
|
19
|
+
user?: string;
|
|
20
|
+
timeout?: number; // ms, default 120000 (2 min)
|
|
21
|
+
maxRetries?: number;
|
|
22
|
+
strictHostKeyChecking?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SSHResult {
|
|
26
|
+
success: boolean;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
elapsedMs: number;
|
|
30
|
+
retries: number;
|
|
31
|
+
hungProcessesKilled: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const DEFAULT_TIMEOUT = 120_000; // 2 minutes
|
|
36
|
+
const HANG_THRESHOLD_MS = 60_000; // 1 minute = likely hang
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize input for shell commands - prevent command injection
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Execute SSH command with automatic hang recovery
|
|
44
|
+
*/
|
|
45
|
+
export async function sshWithRecovery(
|
|
46
|
+
config: SSHConfig,
|
|
47
|
+
command: string,
|
|
48
|
+
): Promise<SSHResult> {
|
|
49
|
+
const {
|
|
50
|
+
host,
|
|
51
|
+
user = "frappe",
|
|
52
|
+
timeout = DEFAULT_TIMEOUT,
|
|
53
|
+
maxRetries = 3,
|
|
54
|
+
strictHostKeyChecking = false,
|
|
55
|
+
} = config;
|
|
56
|
+
|
|
57
|
+
const sshBase = `ssh -o BatchMode=yes -o ConnectTimeout=30 -o StrictHostKeyChecking=${strictHostKeyChecking ? "yes" : "no"} ${user}@${host}`;
|
|
58
|
+
|
|
59
|
+
let retries = 0;
|
|
60
|
+
let hungProcessesKilled = false;
|
|
61
|
+
|
|
62
|
+
while (retries <= maxRetries) {
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// Execute with timeout
|
|
67
|
+
const result = await execAsync(
|
|
68
|
+
`${sshBase} '${command.replace(/'/g, "'\\''")}'`,
|
|
69
|
+
timeout,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: result.exitCode === 0,
|
|
74
|
+
stdout: result.stdout,
|
|
75
|
+
stderr: result.stderr,
|
|
76
|
+
elapsedMs: Date.now() - startTime,
|
|
77
|
+
retries,
|
|
78
|
+
hungProcessesKilled,
|
|
79
|
+
error: result.exitCode === 0 ? undefined : `Exit code: ${result.exitCode}`,
|
|
80
|
+
};
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
const elapsed = Date.now() - startTime;
|
|
83
|
+
const isTimeout = elapsed >= timeout;
|
|
84
|
+
|
|
85
|
+
console.error(
|
|
86
|
+
`[ssh-recovery] Attempt ${retries + 1} failed: ${error.message} (${elapsed}ms)`,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (isTimeout) {
|
|
90
|
+
hungProcessesKilled = true;
|
|
91
|
+
|
|
92
|
+
// Kill stuck processes on remote server
|
|
93
|
+
await killStuckProcesses(host, user);
|
|
94
|
+
|
|
95
|
+
// Also kill any local SSH processes stuck on this host
|
|
96
|
+
killLocalSSHProcesses(host);
|
|
97
|
+
|
|
98
|
+
retries++;
|
|
99
|
+
} else {
|
|
100
|
+
// Non-timeout error (permission denied, etc.)
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
stdout: "",
|
|
104
|
+
stderr: "",
|
|
105
|
+
elapsedMs: elapsed,
|
|
106
|
+
retries,
|
|
107
|
+
hungProcessesKilled: false,
|
|
108
|
+
error: error.message,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// All retries exhausted
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
stdout: "",
|
|
118
|
+
stderr: "",
|
|
119
|
+
elapsedMs: 0,
|
|
120
|
+
retries,
|
|
121
|
+
hungProcessesKilled,
|
|
122
|
+
error: `Command failed after ${maxRetries + 1} attempts`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Kill stuck processes on remote server
|
|
128
|
+
*/
|
|
129
|
+
async function killStuckProcesses(host: string, user: string): Promise<void> {
|
|
130
|
+
const killCommands = [
|
|
131
|
+
// Kill stuck SSH sessions
|
|
132
|
+
`pkill -f "ssh.*${host}" || true`,
|
|
133
|
+
// Kill stuck PM2 commands
|
|
134
|
+
`pkill -f "pm2.*amos-saas" || true`,
|
|
135
|
+
// Kill stuck sudo commands
|
|
136
|
+
`pkill -f "sudo.*pm2" || true`,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
for (const cmd of killCommands) {
|
|
140
|
+
try {
|
|
141
|
+
execSync(
|
|
142
|
+
`ssh -o BatchMode=yes -o ConnectTimeout=10 ${user}@${host} '${cmd}'`,
|
|
143
|
+
{ timeout: 10_000, encoding: "utf-8" },
|
|
144
|
+
);
|
|
145
|
+
} catch {
|
|
146
|
+
// Ignore errors from kill commands
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Kill local SSH processes stuck on this host
|
|
153
|
+
*/
|
|
154
|
+
function killLocalSSHProcesses(host: string): void {
|
|
155
|
+
try {
|
|
156
|
+
// Kill any local SSH processes connecting to this host
|
|
157
|
+
execSync(`pkill -f "ssh.*${host}" || true`, { timeout: 5000 });
|
|
158
|
+
} catch {
|
|
159
|
+
// Ignore
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if server is responsive
|
|
165
|
+
*/
|
|
166
|
+
export async function pingServer(
|
|
167
|
+
host: string,
|
|
168
|
+
user: string = "frappe",
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
try {
|
|
171
|
+
execSync(
|
|
172
|
+
`ssh -o BatchMode=yes -o ConnectTimeout=5 ${user}@${host} 'echo ok'`,
|
|
173
|
+
{ timeout: 10_000 },
|
|
174
|
+
);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get PM2 status with hang protection
|
|
183
|
+
*/
|
|
184
|
+
export async function getPM2Status(
|
|
185
|
+
host: string,
|
|
186
|
+
pm2Path?: string,
|
|
187
|
+
): Promise<SSHResult> {
|
|
188
|
+
const pm2Cmd = pm2Path || "pm2";
|
|
189
|
+
const command = `PM2_HOME=/home/frappe/.pm2 ${pm2Cmd} status`;
|
|
190
|
+
|
|
191
|
+
return sshWithRecovery(
|
|
192
|
+
{
|
|
193
|
+
host,
|
|
194
|
+
timeout: 30_000, // Status should be fast
|
|
195
|
+
maxRetries: 1,
|
|
196
|
+
},
|
|
197
|
+
command,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Restart PM2 process with hang protection
|
|
203
|
+
*/
|
|
204
|
+
export async function pm2Restart(
|
|
205
|
+
host: string,
|
|
206
|
+
app: string,
|
|
207
|
+
): Promise<SSHResult> {
|
|
208
|
+
const command = `PM2_HOME=/home/frappe/.pm2 pm2 restart ${app}`;
|
|
209
|
+
|
|
210
|
+
return sshWithRecovery(
|
|
211
|
+
{
|
|
212
|
+
host,
|
|
213
|
+
timeout: 180_000, // 3 min for restart
|
|
214
|
+
maxRetries: 2,
|
|
215
|
+
},
|
|
216
|
+
command,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Execute arbitrary command with hang protection
|
|
222
|
+
*/
|
|
223
|
+
export async function safeSSHCommand(
|
|
224
|
+
host: string,
|
|
225
|
+
command: string,
|
|
226
|
+
options: { timeout?: number; maxRetries?: number } = {},
|
|
227
|
+
): Promise<SSHResult> {
|
|
228
|
+
return sshWithRecovery(
|
|
229
|
+
{
|
|
230
|
+
host,
|
|
231
|
+
timeout: options.timeout ?? 120_000,
|
|
232
|
+
maxRetries: options.maxRetries ?? 2,
|
|
233
|
+
},
|
|
234
|
+
command,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Helper for promise-based exec with timeout ---
|
|
239
|
+
|
|
240
|
+
interface ExecResult {
|
|
241
|
+
exitCode: number;
|
|
242
|
+
stdout: string;
|
|
243
|
+
stderr: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function execAsync(command: string, timeoutMs: number): Promise<ExecResult> {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const proc = exec(command, { encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
249
|
+
if (error) {
|
|
250
|
+
reject(error);
|
|
251
|
+
} else {
|
|
252
|
+
resolve({ exitCode: 0, stdout, stderr });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const timer = setAsyncTimeout(() => {
|
|
257
|
+
// Kill the process tree
|
|
258
|
+
try {
|
|
259
|
+
proc.kill("SIGKILL");
|
|
260
|
+
} catch {
|
|
261
|
+
// Process already dead
|
|
262
|
+
}
|
|
263
|
+
reject(new Error(`Command timed out after ${timeoutMs}ms`));
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
|
|
266
|
+
proc.on("exit", (code) => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
resolve({
|
|
269
|
+
exitCode: code ?? 0,
|
|
270
|
+
stdout: (proc.stdout || "").toString(),
|
|
271
|
+
stderr: (proc.stderr || "").toString(),
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
proc.on("error", (err) => {
|
|
276
|
+
clearTimeout(timer);
|
|
277
|
+
reject(err);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/capability-registry",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26",
|
|
4
4
|
"description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|