@sireai/optimus 0.1.35 → 0.1.38
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/embedded-skills/task/bugfix/android-hprof-analyzer/SKILL.md +37 -0
- package/embedded-skills/task/bugfix/android-hprof-analyzer/runtime/README.md +11 -0
- package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/analyze-hprof.mjs +286 -0
- package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/ensure-shark-runtime.mjs +213 -0
- package/embedded-skills/task/bugfix/android-hprof-analyzer/scripts/run-shark.sh +27 -0
- package/embedded-skills/task/bugfix/android-hprof-analyzer/skill.json +6 -0
- package/package.json +1 -1
- package/task-harnesses/bugfix/CONSTRAINTS.md +2 -0
- package/task-harnesses/bugfix/ROLE.md +4 -0
- package/task-harnesses/bugfix/STANDARD.md +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Android HPROF Analyzer
|
|
2
|
+
|
|
3
|
+
Use this skill for `bugfix` tasks when any evidence file name contains `hprof`.
|
|
4
|
+
|
|
5
|
+
## Mandatory use
|
|
6
|
+
- If evidence contains a file whose basename includes `hprof`, run this skill before claiming a memory-leak root cause.
|
|
7
|
+
- Do not rely on screenshots alone when an HPROF file is available.
|
|
8
|
+
- Use the generated `summary.json` and `summary.md` as the primary heap-analysis fact source.
|
|
9
|
+
|
|
10
|
+
## Input
|
|
11
|
+
- Preferred: an evidence manifest path plus an output directory.
|
|
12
|
+
- Optional: an explicit evidence directory and an obfuscation mapping file.
|
|
13
|
+
- The Shark CLI runtime is downloaded on demand into `~/.optimus/tools/shark/` the first time this skill runs.
|
|
14
|
+
|
|
15
|
+
## Command
|
|
16
|
+
```bash
|
|
17
|
+
node .agents/skills/android-hprof-analyzer/scripts/analyze-hprof.mjs \
|
|
18
|
+
--manifest <evidence-manifest-path> \
|
|
19
|
+
--output <artifactDir>/hprof-analysis
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Optional flags:
|
|
23
|
+
```bash
|
|
24
|
+
--evidence-dir <dir>
|
|
25
|
+
--mapping <mapping-file>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Output
|
|
29
|
+
- `summary.json`: structured analysis status and per-file metadata
|
|
30
|
+
- `summary.md`: concise human-readable summary with raw output references
|
|
31
|
+
- `raw/*.txt`: raw Shark CLI stdout/stderr for each analyzed dump
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
- Analyze every discovered evidence file whose basename contains `hprof`.
|
|
35
|
+
- If analysis fails, report whether the blocker was missing Java, missing embedded runtime, runner failure, or invalid dump.
|
|
36
|
+
- If first-use setup fails, report whether the download, checksum, or extraction step failed.
|
|
37
|
+
- Mention which HPROF file was analyzed in `result.md`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
This directory documents the runtime source used by the embedded
|
|
2
|
+
`android-hprof-analyzer` bugfix skill.
|
|
3
|
+
|
|
4
|
+
Source release:
|
|
5
|
+
- square/leakcanary `shark-cli-2.14.zip`
|
|
6
|
+
|
|
7
|
+
Runtime behavior:
|
|
8
|
+
- Optimus does not vendor the Shark CLI distribution inside the npm package.
|
|
9
|
+
- The skill downloads the pinned release on first use and caches it under
|
|
10
|
+
`~/.optimus/tools/shark/`.
|
|
11
|
+
- A local Java runtime is still required.
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const SCRIPT_DIR = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const RUNNER_PATH = join(SCRIPT_DIR, "run-shark.sh");
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const options = parseArgs(process.argv.slice(2));
|
|
15
|
+
if (!options.outputDir) {
|
|
16
|
+
throw new Error("analyze-hprof requires --output <dir>.");
|
|
17
|
+
}
|
|
18
|
+
if (!options.manifestPath && !options.evidenceDir) {
|
|
19
|
+
throw new Error("analyze-hprof requires --manifest <path> or --evidence-dir <dir>.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const outputDir = resolve(options.outputDir);
|
|
23
|
+
const rawDir = join(outputDir, "raw");
|
|
24
|
+
await mkdir(rawDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const evidenceFiles = await discoverHprofFiles(options);
|
|
27
|
+
if (evidenceFiles.length === 0) {
|
|
28
|
+
const emptySummary = {
|
|
29
|
+
tool: "embedded-shark",
|
|
30
|
+
status: "failed",
|
|
31
|
+
files: [],
|
|
32
|
+
findings: [],
|
|
33
|
+
warnings: ["No evidence files whose basename contains 'hprof' were found."]
|
|
34
|
+
};
|
|
35
|
+
await writeOutputs(outputDir, emptySummary, ["No HPROF evidence files were found."]);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const files = [];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
for (const [index, filePath] of evidenceFiles.entries()) {
|
|
43
|
+
const rawPath = join(rawDir, `${String(index + 1).padStart(2, "0")}-${sanitizeName(basename(filePath))}.txt`);
|
|
44
|
+
const args = [];
|
|
45
|
+
if (options.mappingPath) {
|
|
46
|
+
args.push("--obfuscation-mapping", resolve(options.mappingPath));
|
|
47
|
+
}
|
|
48
|
+
args.push("--hprof", filePath, "analyze");
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const { stdout, stderr } = await execFileAsync(RUNNER_PATH, args, {
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
54
|
+
env: process.env
|
|
55
|
+
});
|
|
56
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
57
|
+
await writeFile(rawPath, combined || "(no output)\n", "utf8");
|
|
58
|
+
files.push({
|
|
59
|
+
path: filePath,
|
|
60
|
+
analyzed: true,
|
|
61
|
+
rawOutputPath: rawPath,
|
|
62
|
+
findings: extractFindingHints(combined)
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const stdout = error?.stdout ? String(error.stdout) : "";
|
|
66
|
+
const stderr = error?.stderr ? String(error.stderr) : error instanceof Error ? error.message : String(error);
|
|
67
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
68
|
+
await writeFile(rawPath, combined || "(no output)\n", "utf8");
|
|
69
|
+
const reason = classifyRunnerFailure(combined, error);
|
|
70
|
+
warnings.push(`${basename(filePath)}: ${reason}`);
|
|
71
|
+
files.push({
|
|
72
|
+
path: filePath,
|
|
73
|
+
analyzed: false,
|
|
74
|
+
rawOutputPath: rawPath,
|
|
75
|
+
error: reason,
|
|
76
|
+
findings: []
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const findings = files.flatMap((entry) => entry.findings.map((finding) => ({
|
|
82
|
+
sourceFile: entry.path,
|
|
83
|
+
...finding
|
|
84
|
+
})));
|
|
85
|
+
const analyzedCount = files.filter((entry) => entry.analyzed).length;
|
|
86
|
+
const status = analyzedCount === files.length
|
|
87
|
+
? "ok"
|
|
88
|
+
: analyzedCount > 0
|
|
89
|
+
? "partial"
|
|
90
|
+
: "failed";
|
|
91
|
+
const summary = {
|
|
92
|
+
tool: "embedded-shark",
|
|
93
|
+
status,
|
|
94
|
+
files,
|
|
95
|
+
findings,
|
|
96
|
+
warnings
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const markdownLines = [
|
|
100
|
+
"# HPROF Analysis Summary",
|
|
101
|
+
"",
|
|
102
|
+
`- Status: ${status}`,
|
|
103
|
+
`- Files discovered: ${files.length}`,
|
|
104
|
+
`- Files analyzed: ${analyzedCount}`,
|
|
105
|
+
"",
|
|
106
|
+
"## Files"
|
|
107
|
+
];
|
|
108
|
+
for (const entry of files) {
|
|
109
|
+
markdownLines.push(
|
|
110
|
+
`- ${relative(outputDir, entry.path) || entry.path}: ${entry.analyzed ? "analyzed" : `failed (${entry.error})`}`,
|
|
111
|
+
` - Raw Output: ${relative(outputDir, entry.rawOutputPath) || entry.rawOutputPath}`
|
|
112
|
+
);
|
|
113
|
+
for (const finding of entry.findings) {
|
|
114
|
+
markdownLines.push(` - Finding: ${finding.target}${finding.retainPath ? ` | Path: ${finding.retainPath}` : ""}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (warnings.length > 0) {
|
|
118
|
+
markdownLines.push("", "## Warnings");
|
|
119
|
+
for (const warning of warnings) {
|
|
120
|
+
markdownLines.push(`- ${warning}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await writeOutputs(outputDir, summary, markdownLines);
|
|
125
|
+
if (status === "failed") {
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseArgs(argv) {
|
|
131
|
+
const options = {};
|
|
132
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
133
|
+
const arg = argv[index];
|
|
134
|
+
const next = argv[index + 1];
|
|
135
|
+
if (!arg.startsWith("--")) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (!next || next.startsWith("--")) {
|
|
139
|
+
throw new Error(`Missing value for ${arg}.`);
|
|
140
|
+
}
|
|
141
|
+
if (arg === "--manifest") {
|
|
142
|
+
options.manifestPath = next;
|
|
143
|
+
index += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (arg === "--evidence-dir") {
|
|
147
|
+
options.evidenceDir = next;
|
|
148
|
+
index += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === "--output") {
|
|
152
|
+
options.outputDir = next;
|
|
153
|
+
index += 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (arg === "--mapping") {
|
|
157
|
+
options.mappingPath = next;
|
|
158
|
+
index += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
162
|
+
}
|
|
163
|
+
return options;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function discoverHprofFiles(options) {
|
|
167
|
+
const discovered = new Set();
|
|
168
|
+
if (options.manifestPath) {
|
|
169
|
+
const manifestPath = resolve(options.manifestPath);
|
|
170
|
+
const manifestDir = dirname(manifestPath);
|
|
171
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
172
|
+
for (const value of collectPathValues(manifest)) {
|
|
173
|
+
await maybeAddFile(resolve(manifestDir, value), discovered);
|
|
174
|
+
}
|
|
175
|
+
await walkForHprofFiles(manifestDir, discovered);
|
|
176
|
+
}
|
|
177
|
+
if (options.evidenceDir) {
|
|
178
|
+
await walkForHprofFiles(resolve(options.evidenceDir), discovered);
|
|
179
|
+
}
|
|
180
|
+
return [...discovered].sort((left, right) => left.localeCompare(right));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectPathValues(node, values = []) {
|
|
184
|
+
if (!node) {
|
|
185
|
+
return values;
|
|
186
|
+
}
|
|
187
|
+
if (Array.isArray(node)) {
|
|
188
|
+
for (const item of node) {
|
|
189
|
+
collectPathValues(item, values);
|
|
190
|
+
}
|
|
191
|
+
return values;
|
|
192
|
+
}
|
|
193
|
+
if (typeof node === "object") {
|
|
194
|
+
for (const [key, value] of Object.entries(node)) {
|
|
195
|
+
if (typeof value === "string" && key.toLowerCase().endsWith("path")) {
|
|
196
|
+
values.push(value);
|
|
197
|
+
} else {
|
|
198
|
+
collectPathValues(value, values);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return values;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function walkForHprofFiles(rootDir, discovered) {
|
|
206
|
+
let entries = [];
|
|
207
|
+
try {
|
|
208
|
+
entries = await readdir(rootDir, { withFileTypes: true });
|
|
209
|
+
} catch {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
const entryPath = join(rootDir, entry.name);
|
|
214
|
+
if (entry.isDirectory()) {
|
|
215
|
+
await walkForHprofFiles(entryPath, discovered);
|
|
216
|
+
} else if (entry.isFile()) {
|
|
217
|
+
await maybeAddFile(entryPath, discovered);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function maybeAddFile(filePath, discovered) {
|
|
223
|
+
if (!basename(filePath).toLowerCase().includes("hprof")) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const info = await stat(filePath);
|
|
228
|
+
if (info.isFile()) {
|
|
229
|
+
discovered.add(filePath);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// Ignore missing paths recorded in manifests.
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function extractFindingHints(output) {
|
|
237
|
+
if (!output) {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
const hints = [];
|
|
241
|
+
const retainedPattern = /([A-Za-z0-9_$.]+)\s+leaking/giu;
|
|
242
|
+
const pathPattern = /GC Root(?:.|\n){0,300}/iu;
|
|
243
|
+
const targets = [...output.matchAll(retainedPattern)].map((match) => match[1]).slice(0, 5);
|
|
244
|
+
const pathMatch = output.match(pathPattern)?.[0]?.replace(/\s+/gu, " ").trim();
|
|
245
|
+
for (const target of targets) {
|
|
246
|
+
hints.push({
|
|
247
|
+
target,
|
|
248
|
+
...(pathMatch ? { retainPath: pathMatch } : {})
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return hints;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function classifyRunnerFailure(output, error) {
|
|
255
|
+
const text = `${output}\n${error instanceof Error ? error.message : ""}`.toLowerCase();
|
|
256
|
+
if (text.includes("java runtime is required") || text.includes("no 'java' command could be found")) {
|
|
257
|
+
return "missing_java";
|
|
258
|
+
}
|
|
259
|
+
if (text.includes("embedded shark runner is unavailable")) {
|
|
260
|
+
return "missing_embedded_runtime";
|
|
261
|
+
}
|
|
262
|
+
if (text.includes("file not found") || text.includes("no such file")) {
|
|
263
|
+
return "missing_hprof_file";
|
|
264
|
+
}
|
|
265
|
+
if (text.includes("heap dump") || text.includes("hprof")) {
|
|
266
|
+
return "runner_failed";
|
|
267
|
+
}
|
|
268
|
+
return "runner_failed";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function writeOutputs(outputDir, summary, markdownLines) {
|
|
272
|
+
await mkdir(outputDir, { recursive: true });
|
|
273
|
+
await writeFile(join(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
274
|
+
await writeFile(join(outputDir, "summary.md"), `${markdownLines.join("\n")}\n`, "utf8");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function sanitizeName(name) {
|
|
278
|
+
const extension = extname(name);
|
|
279
|
+
const base = extension ? name.slice(0, -extension.length) : name;
|
|
280
|
+
return `${base.replace(/[^\w.-]+/gu, "_").slice(0, 120) || "hprof"}${extension || ".txt"}`.replace(/\.txt$/u, "");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
main().catch((error) => {
|
|
284
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
6
|
+
import { access, appendFile, chmod, copyFile, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
|
|
7
|
+
import { get } from "node:https";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
const SHARK_VERSION = process.env.OPTIMUS_HPROF_SHARK_VERSION?.trim() || "2.14";
|
|
15
|
+
const ARCHIVE_NAME = `shark-cli-${SHARK_VERSION}.zip`;
|
|
16
|
+
const ARCHIVE_DIR_NAME = `shark-cli-${SHARK_VERSION}`;
|
|
17
|
+
const DEFAULT_ARCHIVE_URL = `https://github.com/square/leakcanary/releases/download/v${SHARK_VERSION}/${ARCHIVE_NAME}`;
|
|
18
|
+
const DEFAULT_ARCHIVE_SHA256 = "4a1022a4610fd6a4a1306b264f95985c4210e169e2bd4b0ad19bbdcc16d6beef";
|
|
19
|
+
const SCRIPT_DIR = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
20
|
+
const LOG_PREFIX = "[optimus][hprof-runtime]";
|
|
21
|
+
let logFilePathPromise;
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const cacheRoot = resolve(
|
|
25
|
+
process.env.OPTIMUS_HPROF_CACHE_DIR?.trim()
|
|
26
|
+
|| join(process.env.HOME?.trim() || homedir(), ".optimus", "tools", "shark")
|
|
27
|
+
);
|
|
28
|
+
const archiveUrl = process.env.OPTIMUS_HPROF_SHARK_URL?.trim() || DEFAULT_ARCHIVE_URL;
|
|
29
|
+
const archiveSha256 = process.env.OPTIMUS_HPROF_SHARK_SHA256?.trim() || DEFAULT_ARCHIVE_SHA256;
|
|
30
|
+
const archivePathOverride = process.env.OPTIMUS_HPROF_SHARK_ARCHIVE?.trim();
|
|
31
|
+
const installDir = join(cacheRoot, ARCHIVE_DIR_NAME);
|
|
32
|
+
const runnerPath = join(installDir, "bin", "shark-cli");
|
|
33
|
+
|
|
34
|
+
if (await isExecutableFile(runnerPath)) {
|
|
35
|
+
logInfo(`Using cached Shark runtime at ${installDir}.`);
|
|
36
|
+
process.stdout.write(runnerPath);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
41
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "optimus-shark-install-"));
|
|
42
|
+
logInfo(`Preparing Shark runtime ${SHARK_VERSION} under ${cacheRoot}.`);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const archivePath = archivePathOverride ? resolve(archivePathOverride) : join(tempRoot, ARCHIVE_NAME);
|
|
46
|
+
if (!archivePathOverride) {
|
|
47
|
+
logInfo(`Downloading Shark runtime ${SHARK_VERSION} from ${archiveUrl}.`);
|
|
48
|
+
await downloadArchive(archiveUrl, archivePath);
|
|
49
|
+
logInfo(`Downloaded Shark archive to ${archivePath}.`);
|
|
50
|
+
} else {
|
|
51
|
+
logInfo(`Using provided Shark archive at ${archivePath}.`);
|
|
52
|
+
}
|
|
53
|
+
logInfo(`Verifying Shark archive checksum (${archiveSha256.slice(0, 12)}...).`);
|
|
54
|
+
await verifyArchiveChecksum(archivePath, archiveSha256);
|
|
55
|
+
logInfo("Checksum verified.");
|
|
56
|
+
|
|
57
|
+
const extractRoot = join(tempRoot, "extract");
|
|
58
|
+
await mkdir(extractRoot, { recursive: true });
|
|
59
|
+
logInfo(`Extracting Shark archive into ${extractRoot}.`);
|
|
60
|
+
await extractArchive(archivePath, extractRoot);
|
|
61
|
+
logInfo("Archive extracted.");
|
|
62
|
+
|
|
63
|
+
const extractedDir = join(extractRoot, ARCHIVE_DIR_NAME);
|
|
64
|
+
const extractedRunnerPath = join(extractedDir, "bin", "shark-cli");
|
|
65
|
+
if (!(await isExecutableFile(extractedRunnerPath))) {
|
|
66
|
+
throw new Error(`Downloaded Shark archive did not contain an executable runner at ${extractedRunnerPath}.`);
|
|
67
|
+
}
|
|
68
|
+
await chmod(extractedRunnerPath, 0o755);
|
|
69
|
+
|
|
70
|
+
const stagingDir = join(cacheRoot, `.installing-${Date.now()}-${process.pid}`);
|
|
71
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
72
|
+
await rename(extractedDir, stagingDir);
|
|
73
|
+
try {
|
|
74
|
+
await rename(stagingDir, installDir);
|
|
75
|
+
logInfo(`Installed Shark runtime at ${installDir}.`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (await isExecutableFile(runnerPath)) {
|
|
78
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
79
|
+
logInfo(`Another process finished installation first; reusing cached runtime at ${installDir}.`);
|
|
80
|
+
} else {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.stdout.write(runnerPath);
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function downloadArchive(url, destinationPath) {
|
|
92
|
+
if (url.startsWith("file://")) {
|
|
93
|
+
await copyFile(fileURLToPath(url), destinationPath);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!url.startsWith("https://")) {
|
|
97
|
+
throw new Error(`Unsupported Shark archive URL: ${url}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
101
|
+
const request = get(url, (response) => {
|
|
102
|
+
if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
103
|
+
response.resume();
|
|
104
|
+
downloadArchive(response.headers.location, destinationPath).then(resolvePromise, rejectPromise);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (response.statusCode !== 200) {
|
|
108
|
+
response.resume();
|
|
109
|
+
rejectPromise(new Error(`download_failed: HTTP ${response.statusCode ?? "unknown"}`));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const output = createWriteStream(destinationPath);
|
|
114
|
+
output.on("finish", () => {
|
|
115
|
+
output.close();
|
|
116
|
+
resolvePromise(undefined);
|
|
117
|
+
});
|
|
118
|
+
output.on("error", rejectPromise);
|
|
119
|
+
response.on("error", rejectPromise);
|
|
120
|
+
response.pipe(output);
|
|
121
|
+
});
|
|
122
|
+
request.on("error", rejectPromise);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function verifyArchiveChecksum(archivePath, expectedSha256) {
|
|
127
|
+
const actualSha256 = await computeSha256(archivePath);
|
|
128
|
+
if (actualSha256 !== expectedSha256.toLowerCase()) {
|
|
129
|
+
throw new Error(`checksum_mismatch: expected ${expectedSha256}, received ${actualSha256}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function computeSha256(filePath) {
|
|
134
|
+
const hash = createHash("sha256");
|
|
135
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
136
|
+
const input = createReadStream(filePath);
|
|
137
|
+
input.on("data", (chunk) => hash.update(chunk));
|
|
138
|
+
input.on("end", resolvePromise);
|
|
139
|
+
input.on("error", rejectPromise);
|
|
140
|
+
});
|
|
141
|
+
return hash.digest("hex");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function extractArchive(archivePath, extractRoot) {
|
|
145
|
+
try {
|
|
146
|
+
await execFileAsync("unzip", ["-oq", archivePath, "-d", extractRoot], {
|
|
147
|
+
cwd: SCRIPT_DIR,
|
|
148
|
+
maxBuffer: 16 * 1024 * 1024
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error(`extract_failed: ${formatProcessError(error)}`.trim());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatProcessError(error) {
|
|
156
|
+
if (!error || typeof error !== "object") {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
const stdout = "stdout" in error && error.stdout ? String(error.stdout).trim() : "";
|
|
160
|
+
const stderr = "stderr" in error && error.stderr ? String(error.stderr).trim() : "";
|
|
161
|
+
return [stdout, stderr].filter(Boolean).join(" ").slice(0, 500);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function isExecutableFile(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
await access(filePath);
|
|
167
|
+
const info = await stat(filePath);
|
|
168
|
+
return info.isFile();
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function logInfo(message) {
|
|
175
|
+
void writeLogLine(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
main().catch((error) => {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
void writeLogLine(`Failed to prepare Shark runtime: ${message}`);
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
async function writeLogLine(message) {
|
|
185
|
+
const line = `${LOG_PREFIX} ${message}`;
|
|
186
|
+
process.stderr.write(`${line}\n`);
|
|
187
|
+
try {
|
|
188
|
+
const logPath = await resolveLogFilePath();
|
|
189
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
190
|
+
await appendFile(logPath, `${new Date().toISOString()} ${line}\n`, "utf8");
|
|
191
|
+
} catch {
|
|
192
|
+
// Best-effort logging only; stderr remains the primary fallback.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolveLogFilePath() {
|
|
197
|
+
if (!logFilePathPromise) {
|
|
198
|
+
const homeDir = process.env.HOME?.trim() || homedir();
|
|
199
|
+
const runtimeLogsDir = process.env.OPTIMUS_RUNTIME_LOGS_DIR?.trim()
|
|
200
|
+
? resolve(process.env.OPTIMUS_RUNTIME_LOGS_DIR)
|
|
201
|
+
: join(homeDir, ".optimus", "runtime", "logs");
|
|
202
|
+
logFilePathPromise = Promise.resolve(join(runtimeLogsDir, `runtime-${currentDatePart()}.log`));
|
|
203
|
+
}
|
|
204
|
+
return logFilePathPromise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function currentDatePart() {
|
|
208
|
+
const now = new Date();
|
|
209
|
+
const year = String(now.getFullYear());
|
|
210
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
211
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
212
|
+
return `${year}-${month}-${day}`;
|
|
213
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
5
|
+
ENSURE_SCRIPT="${SCRIPT_DIR}/ensure-shark-runtime.mjs"
|
|
6
|
+
|
|
7
|
+
if [[ -n "${OPTIMUS_HPROF_SHARK_RUNNER:-}" ]]; then
|
|
8
|
+
RUNNER="${OPTIMUS_HPROF_SHARK_RUNNER}"
|
|
9
|
+
else
|
|
10
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
11
|
+
echo "Node.js is required to provision the embedded Shark runtime but was not found in PATH." >&2
|
|
12
|
+
exit 2
|
|
13
|
+
fi
|
|
14
|
+
RUNNER="$(node "${ENSURE_SCRIPT}")"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [[ ! -x "${RUNNER}" ]]; then
|
|
18
|
+
echo "Embedded Shark runner is unavailable: ${RUNNER}" >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if ! command -v java >/dev/null 2>&1; then
|
|
23
|
+
echo "Java runtime is required for embedded Shark analysis but was not found in PATH." >&2
|
|
24
|
+
exit 3
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
exec "${RUNNER}" "$@"
|
package/package.json
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
- Do not present guesses, plans, recommendations, or unexecuted steps as completed facts.
|
|
6
6
|
- Do not claim a fix without evidence.
|
|
7
7
|
- If you ran checks, tests, builds, runtime validation, or manual verification, report what ran, how, what happened, and what remains unverified.
|
|
8
|
+
- If available evidence contains any file whose basename includes `hprof`, do not skip heap-dump analysis before concluding a memory leak.
|
|
9
|
+
- Do not prefer screenshot-only or description-only leak reasoning over available HPROF evidence.
|
|
8
10
|
|
|
9
11
|
## Patch rules
|
|
10
12
|
- Change code only after reasoning through module boundaries, call chains, state flow, and upstream/downstream impact.
|
|
@@ -20,6 +20,10 @@ Typical cases:
|
|
|
20
20
|
- application code, scripts, configuration, build logic, or tests
|
|
21
21
|
- crashes, runtime errors, incorrect behavior, state bugs, and boundary-condition defects
|
|
22
22
|
|
|
23
|
+
## Evidence priority
|
|
24
|
+
- If available evidence contains any file whose basename includes `hprof`, analyze that heap dump before claiming a memory-leak root cause.
|
|
25
|
+
- Treat generated heap-analysis artifacts as primary evidence for memory-retention conclusions.
|
|
26
|
+
|
|
23
27
|
## Non-goals
|
|
24
28
|
You are not:
|
|
25
29
|
- the triage agent deciding whether the task should be accepted
|
|
@@ -93,6 +93,7 @@ Never overstate:
|
|
|
93
93
|
- If code changed, runtime should also emit `patch.diff`.
|
|
94
94
|
- If `patch.diff` exists, `Closure Level` must not be `Analysis Only`.
|
|
95
95
|
- If `patch.diff` exists, Patch Closure Mode is mandatory.
|
|
96
|
+
- If available evidence contains any file whose basename includes `hprof`, state whether the dump was analyzed and identify the strongest file used.
|
|
96
97
|
- Before writing `result.md`, determine `Closure Level`, then follow exactly one language mode:
|
|
97
98
|
- `Verified Fix` or `Repair Candidate`: Patch Closure Mode; all narrative sections are English
|
|
98
99
|
- `Analysis Only`: Analysis Closure Mode; narrative sections are Chinese
|