@sireai/optimus 0.1.34 → 0.1.37
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/dist/cli/optimus.js +41 -22
- package/dist/cli/optimus.js.map +1 -1
- package/dist/integrations/jira/jira-access-manager.d.ts +1 -0
- package/dist/integrations/jira/jira-access-manager.js +24 -0
- package/dist/integrations/jira/jira-access-manager.js.map +1 -1
- package/dist/problem-solving-core/codex/codex-runner.d.ts +2 -0
- package/dist/problem-solving-core/codex/codex-runner.js +43 -28
- package/dist/problem-solving-core/codex/codex-runner.js.map +1 -1
- package/dist/task-environment/delivery/task-delivery-service.js +3 -1
- package/dist/task-environment/delivery/task-delivery-service.js.map +1 -1
- package/dist/task-environment/evidence/evidence-preparation-service.js +2 -1
- package/dist/task-environment/evidence/evidence-preparation-service.js.map +1 -1
- package/dist/task-environment/intake/manual-problem-intake.js +30 -0
- package/dist/task-environment/intake/manual-problem-intake.js.map +1 -1
- package/dist/task-environment/orchestration/execution-context-assembler.d.ts +1 -0
- package/dist/task-environment/orchestration/execution-context-assembler.js +50 -0
- package/dist/task-environment/orchestration/execution-context-assembler.js.map +1 -1
- package/dist/task-environment/orchestration/task-orchestrator.d.ts +1 -0
- package/dist/task-environment/orchestration/task-orchestrator.js +27 -5
- package/dist/task-environment/orchestration/task-orchestrator.js.map +1 -1
- package/dist/task-environment/orchestration/task-package-inputs.d.ts +2 -0
- package/dist/task-environment/orchestration/task-package-inputs.js +38 -0
- package/dist/task-environment/orchestration/task-package-inputs.js.map +1 -0
- package/dist/task-environment/orchestration/task-runtime-policy.d.ts +4 -0
- package/dist/task-environment/orchestration/task-runtime-policy.js +38 -0
- package/dist/task-environment/orchestration/task-runtime-policy.js.map +1 -0
- package/dist/types.d.ts +32 -0
- 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
- package/task-harnesses/pm/ACCEPT.md +94 -0
- package/task-harnesses/pm/CONSTRAINTS.md +27 -0
- package/task-harnesses/pm/CONTEXT.md +26 -0
- package/task-harnesses/pm/EVOLUTION.md +35 -0
- package/task-harnesses/pm/ROLE.md +59 -0
- package/task-harnesses/pm/STANDARD.md +125 -0
- package/task-harnesses/pm/manifest.json +13 -0
- package/task-harnesses/registry.json +4 -0
|
@@ -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/shark-cli-${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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ACCEPT
|
|
2
|
+
|
|
3
|
+
## Decision target
|
|
4
|
+
Route requirement-to-prototype tasks into the `pm` harness.
|
|
5
|
+
|
|
6
|
+
Triage must decide:
|
|
7
|
+
1. task type fit
|
|
8
|
+
2. execution admission
|
|
9
|
+
|
|
10
|
+
The runner, not triage, decides whether final closure is `Prototype Complete`, `Prototype Partial`, or `Analysis Only`.
|
|
11
|
+
|
|
12
|
+
## Minimum input contract
|
|
13
|
+
Treat the task as execution-ready when the request provides a usable requirement basis plus enough structure to prototype one bounded flow.
|
|
14
|
+
|
|
15
|
+
Recommended PM input vocabulary:
|
|
16
|
+
- `requirement_document`: the requirement basis, usually carried by manual submit `description`, `description-file`, or explicit `requirement-doc-file`
|
|
17
|
+
- `product_goal`: the product or business objective
|
|
18
|
+
- `target_user`: the primary user or audience
|
|
19
|
+
- `core_flow`: the main interaction path to prototype
|
|
20
|
+
- `prototype_scope`: the bounded slice to cover in one task
|
|
21
|
+
- `constraints`: platform, channel, or prototype limits when they materially affect output
|
|
22
|
+
|
|
23
|
+
Admission rule:
|
|
24
|
+
- `requirement_document` must be present
|
|
25
|
+
- at least one concrete `core_flow` must be explicit or clearly derivable from the requirement basis
|
|
26
|
+
- `prototype_scope` must already be bounded in the request or easy to bound without inventing product strategy
|
|
27
|
+
- `product_goal` and `target_user` should be present when they affect flow framing, prioritization, or screen meaning
|
|
28
|
+
- `constraints` are required only when platform or delivery limits materially change the prototype
|
|
29
|
+
|
|
30
|
+
## Task type fit
|
|
31
|
+
Classify as `pm` only when all are true:
|
|
32
|
+
- the request is to turn requirement input into an interactive HTML prototype
|
|
33
|
+
- the expected output is a prototype artifact, not production code
|
|
34
|
+
- the task centers on flow, structure, interaction, or state presentation
|
|
35
|
+
- the prototype can be derived from requirement input without real system implementation
|
|
36
|
+
|
|
37
|
+
Do not classify as `pm` when any are true:
|
|
38
|
+
- the request is only strategy discussion, prioritization, or product advice
|
|
39
|
+
- the request is for production frontend/backend implementation
|
|
40
|
+
- the request is only visual design refinement with no requirement-to-prototype goal
|
|
41
|
+
- the request is only PRD writing or requirement analysis with no interactive output expectation
|
|
42
|
+
- the request is a bugfix, code-change, or repository task
|
|
43
|
+
|
|
44
|
+
## Execution admission
|
|
45
|
+
Accept into execution only when all are true:
|
|
46
|
+
- the input provides a usable requirement basis
|
|
47
|
+
- the input provides at least one concrete goal
|
|
48
|
+
- the input provides at least one concrete flow, page path, or interaction path
|
|
49
|
+
- the prototype scope is bounded enough for one task
|
|
50
|
+
- the task does not depend on repository coupling or production-system integration
|
|
51
|
+
|
|
52
|
+
## Still acceptable with partial information
|
|
53
|
+
Still acceptable when:
|
|
54
|
+
- some states, rules, copy, or edge cases are missing
|
|
55
|
+
- but the main objective and at least one core flow are clear
|
|
56
|
+
- and missing detail can be surfaced as assumptions rather than hidden invention
|
|
57
|
+
|
|
58
|
+
## Reject for insufficient execution context
|
|
59
|
+
Reject even if the task fits `pm` when any are true:
|
|
60
|
+
- there is no concrete requirement, scenario, or flow to prototype
|
|
61
|
+
- the input only says "make a prototype" or "design a page" with no clear objective or path
|
|
62
|
+
- multiple unrelated areas are mixed with no bounded scope
|
|
63
|
+
- the input is too abstract to determine what users can do in the prototype
|
|
64
|
+
- the task depends on hidden context not present in the current input
|
|
65
|
+
- the request expects product decisions to be invented from scratch
|
|
66
|
+
- the request expects real implementation instead of prototype behavior
|
|
67
|
+
|
|
68
|
+
## Missing information to mention first when rejecting
|
|
69
|
+
- `requirement_document`
|
|
70
|
+
- `product_goal`
|
|
71
|
+
- `target_user`
|
|
72
|
+
- `core_flow`
|
|
73
|
+
- `prototype_scope`
|
|
74
|
+
- `constraints`
|
|
75
|
+
|
|
76
|
+
## Missing information mapping guidance
|
|
77
|
+
- use `requirement_document` when there is no usable requirement basis in description, attachment, or referenced input
|
|
78
|
+
- use `product_goal` when the business or user objective is unclear
|
|
79
|
+
- use `target_user` when the intended user is unknown
|
|
80
|
+
- use `core_flow` when no concrete flow or interaction path is described
|
|
81
|
+
- use `prototype_scope` when the request is too broad for one prototype task
|
|
82
|
+
- use `constraints` when platform, channel, or product limits are required but missing
|
|
83
|
+
- prefer the smallest set of fields that explains the rejection
|
|
84
|
+
|
|
85
|
+
## Event scope
|
|
86
|
+
- `problem.discovered`
|
|
87
|
+
- `task.submitted_manually`
|
|
88
|
+
|
|
89
|
+
## Triage guidance
|
|
90
|
+
- separate prototype-task fit from execution readiness
|
|
91
|
+
- accept requirement-driven prototype work, not open-ended consulting
|
|
92
|
+
- prefer one clear prototype objective over broad redesign asks
|
|
93
|
+
- incomplete detail is acceptable if the core flow is still prototype-able
|
|
94
|
+
- triage only decides whether the task enters the `pm` pipeline
|