@h-rig/server 0.0.6-alpha.2 → 0.0.6-alpha.4
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/src/index.js +135 -36
- package/dist/src/server-helpers/http-router.js +338 -4
- package/dist/src/server-helpers/issue-analysis.js +4 -1
- package/dist/src/server-helpers/run-mutations.js +102 -72
- package/dist/src/server-helpers/ws-router.js +1 -1
- package/dist/src/server.js +135 -36
- package/package.json +4 -4
package/dist/src/index.js
CHANGED
|
@@ -2970,7 +2970,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
2970
2970
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
2971
2971
|
return async ({ prompt }) => {
|
|
2972
2972
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
2973
|
-
const
|
|
2973
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
2974
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
2975
|
+
if (provider)
|
|
2976
|
+
args.push("--provider", provider);
|
|
2974
2977
|
if (model)
|
|
2975
2978
|
args.push("--model", model);
|
|
2976
2979
|
args.push(prompt);
|
|
@@ -4380,11 +4383,23 @@ function assertNoActiveRunForTask(projectRoot, taskId, newRunId) {
|
|
|
4380
4383
|
return;
|
|
4381
4384
|
throw new Error(`Task ${taskId} already has an active Rig run: ${existing.runId}`);
|
|
4382
4385
|
}
|
|
4386
|
+
async function resolveSourceTaskForRun(projectRoot, taskId, readTasks) {
|
|
4387
|
+
const fromReader = (await readTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4388
|
+
if (fromReader)
|
|
4389
|
+
return fromReader;
|
|
4390
|
+
const projected = readTaskProjection(projectRoot)?.tasks.find((task) => String(task.id) === taskId) ?? null;
|
|
4391
|
+
if (projected)
|
|
4392
|
+
return projected;
|
|
4393
|
+
if (readTasks !== readWorkspaceTasks) {
|
|
4394
|
+
return (await readWorkspaceTasks(projectRoot)).find((task) => task.id === taskId) ?? null;
|
|
4395
|
+
}
|
|
4396
|
+
return null;
|
|
4397
|
+
}
|
|
4383
4398
|
async function createRunRecord(projectRoot, input, readTasks = readWorkspaceTasks) {
|
|
4384
4399
|
if ("taskId" in input && input.taskId) {
|
|
4385
4400
|
assertNoActiveRunForTask(projectRoot, input.taskId, input.runId);
|
|
4386
4401
|
}
|
|
4387
|
-
const sourceTask = "taskId" in input && input.taskId ?
|
|
4402
|
+
const sourceTask = "taskId" in input && input.taskId ? await resolveSourceTaskForRun(projectRoot, input.taskId, readTasks) : null;
|
|
4388
4403
|
const taskTitle = sourceTask?.title ?? ("taskId" in input && input.taskId ? input.taskId : null);
|
|
4389
4404
|
const runDir = resolveAuthorityRunDir3(projectRoot, input.runId);
|
|
4390
4405
|
const runRecord = {
|
|
@@ -4456,6 +4471,7 @@ async function startLocalRun(state, runId, options) {
|
|
|
4456
4471
|
throw new Error(`Run not found: ${runId}`);
|
|
4457
4472
|
}
|
|
4458
4473
|
const startedAt = new Date().toISOString();
|
|
4474
|
+
const resumeMode = options?.resume === true;
|
|
4459
4475
|
state.runProcesses.set(runId, {
|
|
4460
4476
|
runId,
|
|
4461
4477
|
child: null,
|
|
@@ -4472,9 +4488,9 @@ async function startLocalRun(state, runId, options) {
|
|
|
4472
4488
|
summary: run.title
|
|
4473
4489
|
});
|
|
4474
4490
|
appendRunLogEntry(state.projectRoot, runId, {
|
|
4475
|
-
id: `log:${runId}:prepare`,
|
|
4476
|
-
title: "Rig task run starting",
|
|
4477
|
-
detail: run.taskId ?? run.title,
|
|
4491
|
+
id: `log:${runId}:${resumeMode ? "resume" : "prepare"}`,
|
|
4492
|
+
title: resumeMode ? "Rig task run resuming" : "Rig task run starting",
|
|
4493
|
+
detail: resumeMode ? `Resuming ${run.taskId ?? run.title ?? runId} after server restart or operator resume.` : run.taskId ?? run.title,
|
|
4478
4494
|
tone: "info",
|
|
4479
4495
|
status: "preparing",
|
|
4480
4496
|
createdAt: startedAt
|
|
@@ -4552,7 +4568,15 @@ async function startLocalRun(state, runId, options) {
|
|
|
4552
4568
|
RIG_SERVER_INTERNAL_EXEC: "1",
|
|
4553
4569
|
...serverUrl ? { RIG_SERVER_URL: serverUrl } : {},
|
|
4554
4570
|
...bridgeAuthToken ? { RIG_AUTH_TOKEN: bridgeAuthToken } : {},
|
|
4555
|
-
...bridgeGitHubToken ? {
|
|
4571
|
+
...bridgeGitHubToken ? {
|
|
4572
|
+
RIG_GITHUB_TOKEN: bridgeGitHubToken,
|
|
4573
|
+
GITHUB_TOKEN: bridgeGitHubToken,
|
|
4574
|
+
GH_TOKEN: bridgeGitHubToken
|
|
4575
|
+
} : {},
|
|
4576
|
+
...resumeMode ? {
|
|
4577
|
+
RIG_RUN_RESUME: "1",
|
|
4578
|
+
RIG_RUNTIME_ARTIFACT_CLEANUP: "preserve"
|
|
4579
|
+
} : {}
|
|
4556
4580
|
},
|
|
4557
4581
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4558
4582
|
});
|
|
@@ -4724,7 +4748,7 @@ async function resumeRunRecord(state, input) {
|
|
|
4724
4748
|
if (run.status === "completed") {
|
|
4725
4749
|
throw new Error("Completed runs cannot be resumed.");
|
|
4726
4750
|
}
|
|
4727
|
-
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null });
|
|
4751
|
+
await startLocalRun(state, input.runId, { promptOverride: input.promptOverride ?? null, resume: input.restart !== true });
|
|
4728
4752
|
}
|
|
4729
4753
|
function appendRunMessage(projectRoot, input) {
|
|
4730
4754
|
const run = readAuthorityRun4(projectRoot, input.runId);
|
|
@@ -4808,34 +4832,12 @@ function removeTaskIdsFromQueueState(projectRoot, taskIds) {
|
|
|
4808
4832
|
writeQueueState(projectRoot, next);
|
|
4809
4833
|
return next;
|
|
4810
4834
|
}
|
|
4811
|
-
var
|
|
4812
|
-
function
|
|
4813
|
-
|
|
4814
|
-
for (const run of runs) {
|
|
4835
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
4836
|
+
function collectResumableLocalRuns(state, runs) {
|
|
4837
|
+
return runs.filter((run) => {
|
|
4815
4838
|
const status = normalizeString(run.status)?.toLowerCase() ?? "";
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
if (run.mode !== "local" || !wasStartedByRigServer || !ORPHANABLE_LOCAL_RUN_STATUSES.has(status) || state.runProcesses.has(run.runId)) {
|
|
4819
|
-
continue;
|
|
4820
|
-
}
|
|
4821
|
-
const detail = "Recovered stale local run after Rig server restart; no live child process was attached to this server instance.";
|
|
4822
|
-
patchRunRecord(state.projectRoot, run.runId, {
|
|
4823
|
-
status: "failed",
|
|
4824
|
-
completedAt: run.completedAt ?? nowIso2,
|
|
4825
|
-
updatedAt: nowIso2,
|
|
4826
|
-
errorText: detail
|
|
4827
|
-
});
|
|
4828
|
-
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4829
|
-
id: `log:${run.runId}:stale-local-run`,
|
|
4830
|
-
title: "Run marked stale after server restart",
|
|
4831
|
-
detail,
|
|
4832
|
-
tone: "error",
|
|
4833
|
-
status: "failed",
|
|
4834
|
-
createdAt: nowIso2
|
|
4835
|
-
});
|
|
4836
|
-
changed = true;
|
|
4837
|
-
}
|
|
4838
|
-
return changed;
|
|
4839
|
+
return run.mode === "local" && RESUMABLE_LOCAL_RUN_STATUSES.has(status) && !state.runProcesses.has(run.runId);
|
|
4840
|
+
});
|
|
4839
4841
|
}
|
|
4840
4842
|
async function reconcileScheduler(state, reason) {
|
|
4841
4843
|
if (state.scheduler.reconciling) {
|
|
@@ -4850,7 +4852,20 @@ async function reconcileScheduler(state, reason) {
|
|
|
4850
4852
|
const queue = readQueueState(state.projectRoot);
|
|
4851
4853
|
const tasks = await state.snapshotService.getWorkspaceTasks();
|
|
4852
4854
|
let runs = listAuthorityRuns4(state.projectRoot);
|
|
4853
|
-
let changed =
|
|
4855
|
+
let changed = false;
|
|
4856
|
+
const resumableRuns = collectResumableLocalRuns(state, runs);
|
|
4857
|
+
for (const run of resumableRuns) {
|
|
4858
|
+
appendRunLogEntry(state.projectRoot, run.runId, {
|
|
4859
|
+
id: `log:${run.runId}:auto-resume:${Date.now()}`,
|
|
4860
|
+
title: "Run auto-resume scheduled",
|
|
4861
|
+
detail: `Rig server recovered nonterminal run ${run.runId} after ${reason}; resuming the same lifecycle instead of restarting it.`,
|
|
4862
|
+
tone: "info",
|
|
4863
|
+
status: "preparing",
|
|
4864
|
+
createdAt: new Date().toISOString()
|
|
4865
|
+
});
|
|
4866
|
+
await startLocalRun(state, run.runId, { resume: true });
|
|
4867
|
+
changed = true;
|
|
4868
|
+
}
|
|
4854
4869
|
if (changed) {
|
|
4855
4870
|
runs = listAuthorityRuns4(state.projectRoot);
|
|
4856
4871
|
}
|
|
@@ -6128,6 +6143,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
6128
6143
|
}
|
|
6129
6144
|
return filtered;
|
|
6130
6145
|
}
|
|
6146
|
+
function issueAnalysisTargetFor(source) {
|
|
6147
|
+
if (!source)
|
|
6148
|
+
return null;
|
|
6149
|
+
const candidate = source;
|
|
6150
|
+
if (typeof candidate.updateTask !== "function")
|
|
6151
|
+
return null;
|
|
6152
|
+
return {
|
|
6153
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
6154
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
6155
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
6156
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
6157
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
6158
|
+
};
|
|
6159
|
+
}
|
|
6160
|
+
function uniqueStringList(value) {
|
|
6161
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
6162
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
6163
|
+
}
|
|
6164
|
+
function taskRecordId(task) {
|
|
6165
|
+
return String(task.id ?? "");
|
|
6166
|
+
}
|
|
6131
6167
|
function redactRemoteEndpoint(endpoint) {
|
|
6132
6168
|
const { token, ...rest } = endpoint;
|
|
6133
6169
|
return {
|
|
@@ -6470,6 +6506,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
6470
6506
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
6471
6507
|
});
|
|
6472
6508
|
}
|
|
6509
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
6510
|
+
const body = await deps.readJsonBody(req);
|
|
6511
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
6512
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
6513
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
6514
|
+
return deps.badRequest("ids is required unless all=true");
|
|
6515
|
+
}
|
|
6516
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
6517
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
6518
|
+
const target = issueAnalysisTargetFor(source);
|
|
6519
|
+
if (!source || !target) {
|
|
6520
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
6521
|
+
}
|
|
6522
|
+
const allTasks = [...await source.list()];
|
|
6523
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
6524
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
6525
|
+
if (cached)
|
|
6526
|
+
return cached;
|
|
6527
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
6528
|
+
}))).filter((task) => Boolean(task));
|
|
6529
|
+
if (issues.length === 0) {
|
|
6530
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
6531
|
+
}
|
|
6532
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
6533
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
6534
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
6535
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
6536
|
+
const service = createIssueAnalysisService({
|
|
6537
|
+
analyzer: createPiIssueAnalyzer({
|
|
6538
|
+
...model ? { model } : {},
|
|
6539
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
6540
|
+
}),
|
|
6541
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
6542
|
+
});
|
|
6543
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
6544
|
+
let results;
|
|
6545
|
+
try {
|
|
6546
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
6547
|
+
} catch (error) {
|
|
6548
|
+
return deps.jsonResponse({
|
|
6549
|
+
ok: false,
|
|
6550
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6551
|
+
reason,
|
|
6552
|
+
ids: issues.map((issue) => issue.id)
|
|
6553
|
+
}, 502);
|
|
6554
|
+
}
|
|
6555
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
6556
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
6557
|
+
return;
|
|
6558
|
+
});
|
|
6559
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
6560
|
+
return deps.jsonResponse({
|
|
6561
|
+
ok: true,
|
|
6562
|
+
reason,
|
|
6563
|
+
analyzed: results.map((entry) => ({
|
|
6564
|
+
id: entry.issue.id,
|
|
6565
|
+
title: entry.issue.title ?? null,
|
|
6566
|
+
result: entry.result
|
|
6567
|
+
}))
|
|
6568
|
+
});
|
|
6569
|
+
}
|
|
6473
6570
|
if (url.pathname === "/api/server/status") {
|
|
6474
6571
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
6475
6572
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -7165,11 +7262,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
7165
7262
|
const runId = normalizeString(body.runId);
|
|
7166
7263
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
7167
7264
|
const promptOverride = normalizeString(body.promptOverride);
|
|
7265
|
+
const restart = body.restart === true;
|
|
7168
7266
|
if (!runId) {
|
|
7169
7267
|
return deps.badRequest("runId is required");
|
|
7170
7268
|
}
|
|
7171
7269
|
try {
|
|
7172
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
7270
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
7173
7271
|
deps.broadcastSnapshotInvalidation(state);
|
|
7174
7272
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
7175
7273
|
} catch (error) {
|
|
@@ -13079,6 +13177,7 @@ async function createRigServer(options, projectRoot = resolveProjectRoot()) {
|
|
|
13079
13177
|
const server = Bun.serve({
|
|
13080
13178
|
hostname: options.host,
|
|
13081
13179
|
port: options.port,
|
|
13180
|
+
idleTimeout: Math.max(10, Math.min(255, Number.parseInt(process.env.RIG_SERVER_IDLE_TIMEOUT_SECONDS || "255", 10) || 255)),
|
|
13082
13181
|
fetch: (req, server2) => createRigServerFetch2(state)(req, server2),
|
|
13083
13182
|
websocket: {
|
|
13084
13183
|
open(ws) {
|
|
@@ -469,6 +469,257 @@ async function refreshTaskProjection(projectRoot, input) {
|
|
|
469
469
|
});
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
+
// packages/server/src/server-helpers/issue-analysis.ts
|
|
473
|
+
import { execFile } from "child_process";
|
|
474
|
+
import { createHash } from "crypto";
|
|
475
|
+
function stableIssueHash(issue) {
|
|
476
|
+
const labels = Array.isArray(issue.labels) ? [...issue.labels].map(String).sort() : [];
|
|
477
|
+
const body = typeof issue.body === "string" ? issue.body : "";
|
|
478
|
+
const title = typeof issue.title === "string" ? issue.title : "";
|
|
479
|
+
return createHash("sha256").update(JSON.stringify({ id: issue.id, title, body, labels, deps: issue.deps, status: issue.status })).digest("hex");
|
|
480
|
+
}
|
|
481
|
+
function renderIssueAnalysisPrompt(input) {
|
|
482
|
+
const issue = input.issue;
|
|
483
|
+
const neighbors = input.neighbors ?? [];
|
|
484
|
+
return [
|
|
485
|
+
"You are Rig issue analysis running inside Pi.",
|
|
486
|
+
"Return JSON only with optional metadataPatch, labelsToAdd, labelsToRemove, and generatedIssues.",
|
|
487
|
+
"Preserve all human-authored issue body content. Only propose edits for Rig-owned metadata/status sections, labels, and generated issues.",
|
|
488
|
+
"Generated issues must be concrete, minimal follow-up tasks and will be labeled rig:generated by Rig.",
|
|
489
|
+
"",
|
|
490
|
+
"Issue:",
|
|
491
|
+
JSON.stringify({
|
|
492
|
+
id: issue.id,
|
|
493
|
+
title: issue.title,
|
|
494
|
+
body: issue.body,
|
|
495
|
+
labels: issue.labels,
|
|
496
|
+
deps: issue.deps,
|
|
497
|
+
status: issue.status
|
|
498
|
+
}, null, 2),
|
|
499
|
+
"",
|
|
500
|
+
"Neighbor tasks:",
|
|
501
|
+
JSON.stringify(neighbors.map((task) => ({ id: task.id, title: task.title, status: task.status, deps: task.deps })), null, 2)
|
|
502
|
+
].join(`
|
|
503
|
+
`);
|
|
504
|
+
}
|
|
505
|
+
function isRecord(value) {
|
|
506
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
507
|
+
}
|
|
508
|
+
function stringArray(value) {
|
|
509
|
+
if (!Array.isArray(value))
|
|
510
|
+
return;
|
|
511
|
+
return value.map(String).filter((entry) => entry.trim().length > 0);
|
|
512
|
+
}
|
|
513
|
+
function generatedIssues(value) {
|
|
514
|
+
if (!Array.isArray(value))
|
|
515
|
+
return;
|
|
516
|
+
return value.flatMap((entry) => {
|
|
517
|
+
if (!isRecord(entry) || typeof entry.title !== "string")
|
|
518
|
+
return [];
|
|
519
|
+
return [{
|
|
520
|
+
title: entry.title,
|
|
521
|
+
body: typeof entry.body === "string" ? entry.body : "",
|
|
522
|
+
labels: stringArray(entry.labels) ?? [],
|
|
523
|
+
...Array.isArray(entry.dependsOn) ? { dependsOn: entry.dependsOn.map(String) } : {}
|
|
524
|
+
}];
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
function findJsonLikeText(value) {
|
|
528
|
+
if (typeof value === "string") {
|
|
529
|
+
const trimmed = value.trim();
|
|
530
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("```"))
|
|
531
|
+
return trimmed;
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
if (Array.isArray(value)) {
|
|
535
|
+
for (const entry of value) {
|
|
536
|
+
const found = findJsonLikeText(entry);
|
|
537
|
+
if (found)
|
|
538
|
+
return found;
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
if (!isRecord(value))
|
|
543
|
+
return null;
|
|
544
|
+
for (const key of ["text", "content", "message", "output_text", "response", "stdout"]) {
|
|
545
|
+
const found = findJsonLikeText(value[key]);
|
|
546
|
+
if (found)
|
|
547
|
+
return found;
|
|
548
|
+
}
|
|
549
|
+
for (const entry of Object.values(value)) {
|
|
550
|
+
const found = findJsonLikeText(entry);
|
|
551
|
+
if (found)
|
|
552
|
+
return found;
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
function candidateAnalysisObject(value) {
|
|
557
|
+
if (!isRecord(value))
|
|
558
|
+
return null;
|
|
559
|
+
if (isRecord(value.result))
|
|
560
|
+
return candidateAnalysisObject(value.result) ?? value.result;
|
|
561
|
+
if (isRecord(value.analysis))
|
|
562
|
+
return candidateAnalysisObject(value.analysis) ?? value.analysis;
|
|
563
|
+
if (isRecord(value.metadataPatch) || Array.isArray(value.labelsToAdd) || Array.isArray(value.labelsToRemove) || Array.isArray(value.generatedIssues)) {
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
const nested = findJsonLikeText(value);
|
|
567
|
+
if (nested && nested !== JSON.stringify(value)) {
|
|
568
|
+
try {
|
|
569
|
+
const parsedNested = JSON.parse(nested.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim() ?? nested);
|
|
570
|
+
return candidateAnalysisObject(parsedNested);
|
|
571
|
+
} catch {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
function parseIssueAnalysisResult(raw) {
|
|
578
|
+
let parsed = raw;
|
|
579
|
+
if (typeof raw === "string") {
|
|
580
|
+
const trimmed = raw.trim();
|
|
581
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i)?.[1]?.trim();
|
|
582
|
+
try {
|
|
583
|
+
parsed = JSON.parse(fenced ?? trimmed);
|
|
584
|
+
} catch {
|
|
585
|
+
const lastJsonLine = trimmed.split(/\r?\n/).reverse().find((line) => line.trim().startsWith("{"));
|
|
586
|
+
parsed = lastJsonLine ? JSON.parse(lastJsonLine) : {};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const candidate = candidateAnalysisObject(parsed);
|
|
590
|
+
if (!candidate)
|
|
591
|
+
return {};
|
|
592
|
+
const result = {};
|
|
593
|
+
if (isRecord(candidate.metadataPatch))
|
|
594
|
+
result.metadataPatch = candidate.metadataPatch;
|
|
595
|
+
const add = stringArray(candidate.labelsToAdd);
|
|
596
|
+
if (add?.length)
|
|
597
|
+
result.labelsToAdd = add;
|
|
598
|
+
const remove = stringArray(candidate.labelsToRemove);
|
|
599
|
+
if (remove?.length)
|
|
600
|
+
result.labelsToRemove = remove;
|
|
601
|
+
const generated = generatedIssues(candidate.generatedIssues);
|
|
602
|
+
if (generated?.length)
|
|
603
|
+
result.generatedIssues = generated;
|
|
604
|
+
return result;
|
|
605
|
+
}
|
|
606
|
+
function createDefaultPiIssueAnalysisCommandRunner() {
|
|
607
|
+
return (command, args, options) => new Promise((resolve6) => {
|
|
608
|
+
execFile(command, [...args], {
|
|
609
|
+
timeout: options.timeoutMs,
|
|
610
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
611
|
+
env: options.env ? { ...process.env, ...options.env } : process.env
|
|
612
|
+
}, (error, stdout, stderr) => {
|
|
613
|
+
const exitCode = typeof error?.code === "number" ? error.code : error ? 1 : 0;
|
|
614
|
+
resolve6({ exitCode, stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
function createPiIssueAnalyzer(input = {}) {
|
|
619
|
+
const piBinary = input.piBinary ?? process.env.RIG_ISSUE_ANALYSIS_PI_BINARY ?? "pi";
|
|
620
|
+
const timeoutMs = Math.max(1000, Math.trunc(input.timeoutMs ?? Number(process.env.RIG_ISSUE_ANALYSIS_TIMEOUT_MS ?? 120000)));
|
|
621
|
+
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
622
|
+
return async ({ prompt }) => {
|
|
623
|
+
const args = ["--print", "--mode", "json", "--no-session"];
|
|
624
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
625
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
626
|
+
if (provider)
|
|
627
|
+
args.push("--provider", provider);
|
|
628
|
+
if (model)
|
|
629
|
+
args.push("--model", model);
|
|
630
|
+
args.push(prompt);
|
|
631
|
+
const result = await runCommand(piBinary, args, { timeoutMs, ...input.env ? { env: input.env } : {} });
|
|
632
|
+
if (result.exitCode !== 0) {
|
|
633
|
+
throw new Error(`Pi issue analysis failed (exit ${result.exitCode}): ${result.stderr ?? result.stdout}`);
|
|
634
|
+
}
|
|
635
|
+
return parseIssueAnalysisResult(result.stdout);
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function defaultStatusComment(input) {
|
|
639
|
+
const changes = [
|
|
640
|
+
input.result.metadataPatch ? "metadata" : null,
|
|
641
|
+
input.result.labelsToAdd?.length ? `labels added: ${input.result.labelsToAdd.join(", ")}` : null,
|
|
642
|
+
input.result.labelsToRemove?.length ? `labels removed: ${input.result.labelsToRemove.join(", ")}` : null,
|
|
643
|
+
input.result.generatedIssues?.length ? `generated issues: ${input.result.generatedIssues.length}` : null
|
|
644
|
+
].filter((entry) => Boolean(entry));
|
|
645
|
+
if (changes.length === 0)
|
|
646
|
+
return null;
|
|
647
|
+
return [
|
|
648
|
+
"<!-- rig:status-comment -->",
|
|
649
|
+
"### Rig issue analysis",
|
|
650
|
+
"",
|
|
651
|
+
`Analyzed issue ${input.issue.id}${input.reason ? ` (${input.reason})` : ""}.`,
|
|
652
|
+
"",
|
|
653
|
+
...changes.map((change) => `- ${change}`)
|
|
654
|
+
].join(`
|
|
655
|
+
`);
|
|
656
|
+
}
|
|
657
|
+
function uniqueLabels(labels, required = []) {
|
|
658
|
+
return [...new Set([...labels ?? [], ...required].map((label) => label.trim()).filter(Boolean))];
|
|
659
|
+
}
|
|
660
|
+
function createIssueAnalysisWriteBack(input) {
|
|
661
|
+
return async ({ issue, result, reason }) => {
|
|
662
|
+
if (result.metadataPatch && Object.keys(result.metadataPatch).length > 0) {
|
|
663
|
+
if (!input.target.updateTask)
|
|
664
|
+
throw new Error("Issue analysis writeback requires updateTask for metadata patches.");
|
|
665
|
+
await input.target.updateTask(issue.id, { metadata: result.metadataPatch });
|
|
666
|
+
}
|
|
667
|
+
if (result.labelsToAdd?.length) {
|
|
668
|
+
if (!input.target.addLabels)
|
|
669
|
+
throw new Error("Issue analysis writeback requires addLabels for labelsToAdd.");
|
|
670
|
+
await input.target.addLabels(issue.id, uniqueLabels(result.labelsToAdd));
|
|
671
|
+
}
|
|
672
|
+
if (result.labelsToRemove?.length) {
|
|
673
|
+
if (!input.target.removeLabels)
|
|
674
|
+
throw new Error("Issue analysis writeback requires removeLabels for labelsToRemove.");
|
|
675
|
+
await input.target.removeLabels(issue.id, uniqueLabels(result.labelsToRemove));
|
|
676
|
+
}
|
|
677
|
+
const comment = (input.buildStatusComment ?? defaultStatusComment)({ issue, result, reason });
|
|
678
|
+
if (comment?.trim()) {
|
|
679
|
+
if (!input.target.updateTask)
|
|
680
|
+
throw new Error("Issue analysis writeback requires updateTask for sticky status comments.");
|
|
681
|
+
await input.target.updateTask(issue.id, { comment });
|
|
682
|
+
}
|
|
683
|
+
for (const generated of result.generatedIssues ?? []) {
|
|
684
|
+
if (!input.target.createIssue)
|
|
685
|
+
throw new Error("Issue analysis writeback requires createIssue for generated issues.");
|
|
686
|
+
await input.target.createIssue({
|
|
687
|
+
title: generated.title,
|
|
688
|
+
body: generated.dependsOn?.length ? `${generated.body.trimEnd()}
|
|
689
|
+
|
|
690
|
+
depends-on: ${generated.dependsOn.map((dep) => dep.startsWith("#") ? dep : `#${dep}`).join(", ")}
|
|
691
|
+
` : generated.body,
|
|
692
|
+
labels: uniqueLabels(generated.labels, ["rig:generated"])
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function createIssueAnalysisService(input) {
|
|
698
|
+
const analyzedHashes = new Map;
|
|
699
|
+
return {
|
|
700
|
+
async analyze(issues, options = {}) {
|
|
701
|
+
const results = [];
|
|
702
|
+
const neighbors = options.neighbors ?? issues;
|
|
703
|
+
for (const issue of issues) {
|
|
704
|
+
const hash = stableIssueHash(issue);
|
|
705
|
+
if (analyzedHashes.get(issue.id) === hash)
|
|
706
|
+
continue;
|
|
707
|
+
const prompt = renderIssueAnalysisPrompt({ issue, neighbors: neighbors.filter((candidate) => candidate.id !== issue.id) });
|
|
708
|
+
const result = await input.analyzer({ issue, neighbors, prompt });
|
|
709
|
+
analyzedHashes.set(issue.id, hash);
|
|
710
|
+
if (result.metadataPatch || result.labelsToAdd?.length || result.labelsToRemove?.length || result.generatedIssues?.length) {
|
|
711
|
+
await input.writeBack?.({ issue, result, reason: options.reason });
|
|
712
|
+
}
|
|
713
|
+
results.push({ issue, result });
|
|
714
|
+
}
|
|
715
|
+
return results;
|
|
716
|
+
},
|
|
717
|
+
clearCache() {
|
|
718
|
+
analyzedHashes.clear();
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
472
723
|
// packages/server/src/server-helpers/terminal-runtime.ts
|
|
473
724
|
import { WS_CHANNELS as WS_CHANNELS2 } from "@rig/contracts";
|
|
474
725
|
|
|
@@ -916,7 +1167,7 @@ var TERMINAL_RUN_STATUSES2 = new Set([
|
|
|
916
1167
|
"needs-attention",
|
|
917
1168
|
"stopped"
|
|
918
1169
|
]);
|
|
919
|
-
var
|
|
1170
|
+
var RESUMABLE_LOCAL_RUN_STATUSES = new Set(["created", "preparing", "running", "validating", "reviewing"]);
|
|
920
1171
|
|
|
921
1172
|
// packages/server/src/server-helpers/ws-router.ts
|
|
922
1173
|
import {
|
|
@@ -1223,7 +1474,7 @@ import {
|
|
|
1223
1474
|
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1224
1475
|
|
|
1225
1476
|
// packages/server/src/server-helpers/project-registry.ts
|
|
1226
|
-
import { createHash } from "crypto";
|
|
1477
|
+
import { createHash as createHash2 } from "crypto";
|
|
1227
1478
|
import { spawnSync } from "child_process";
|
|
1228
1479
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync5 } from "fs";
|
|
1229
1480
|
import { dirname as dirname4, resolve as resolve9 } from "path";
|
|
@@ -1266,7 +1517,7 @@ function hashFile(path) {
|
|
|
1266
1517
|
if (!path)
|
|
1267
1518
|
return null;
|
|
1268
1519
|
try {
|
|
1269
|
-
return
|
|
1520
|
+
return createHash2("sha256").update(readFileSync4(path)).digest("hex");
|
|
1270
1521
|
} catch {
|
|
1271
1522
|
return null;
|
|
1272
1523
|
}
|
|
@@ -2175,6 +2426,27 @@ function filterWorkspaceTasks(projectRoot, tasks, searchParams) {
|
|
|
2175
2426
|
}
|
|
2176
2427
|
return filtered;
|
|
2177
2428
|
}
|
|
2429
|
+
function issueAnalysisTargetFor(source) {
|
|
2430
|
+
if (!source)
|
|
2431
|
+
return null;
|
|
2432
|
+
const candidate = source;
|
|
2433
|
+
if (typeof candidate.updateTask !== "function")
|
|
2434
|
+
return null;
|
|
2435
|
+
return {
|
|
2436
|
+
...typeof candidate.get === "function" ? { get: candidate.get.bind(candidate) } : {},
|
|
2437
|
+
updateTask: candidate.updateTask.bind(candidate),
|
|
2438
|
+
...typeof candidate.addLabels === "function" ? { addLabels: candidate.addLabels.bind(candidate) } : {},
|
|
2439
|
+
...typeof candidate.removeLabels === "function" ? { removeLabels: candidate.removeLabels.bind(candidate) } : {},
|
|
2440
|
+
...typeof candidate.createIssue === "function" ? { createIssue: candidate.createIssue.bind(candidate) } : {}
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function uniqueStringList(value) {
|
|
2444
|
+
const raw = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
|
|
2445
|
+
return [...new Set(raw.map((entry) => String(entry).trim()).filter(Boolean))];
|
|
2446
|
+
}
|
|
2447
|
+
function taskRecordId(task) {
|
|
2448
|
+
return String(task.id ?? "");
|
|
2449
|
+
}
|
|
2178
2450
|
function redactRemoteEndpoint(endpoint) {
|
|
2179
2451
|
const { token, ...rest } = endpoint;
|
|
2180
2452
|
return {
|
|
@@ -2517,6 +2789,67 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
2517
2789
|
note: "GitHub issue lifecycle labels are created on demand by the configured task source when supported."
|
|
2518
2790
|
});
|
|
2519
2791
|
}
|
|
2792
|
+
if (url.pathname === "/api/workspace/issue-analysis/run" && req.method === "POST") {
|
|
2793
|
+
const body = await deps.readJsonBody(req);
|
|
2794
|
+
const ids = uniqueStringList(body.ids ?? body.id);
|
|
2795
|
+
const analyzeAll = deps.isTruthyQuery(String(body.all ?? ""));
|
|
2796
|
+
if (ids.length === 0 && !analyzeAll) {
|
|
2797
|
+
return deps.badRequest("ids is required unless all=true");
|
|
2798
|
+
}
|
|
2799
|
+
const ctx = await getCachedPluginHostContext(state.projectRoot);
|
|
2800
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
2801
|
+
const target = issueAnalysisTargetFor(source);
|
|
2802
|
+
if (!source || !target) {
|
|
2803
|
+
return deps.badRequest("Configured task source does not support issue-analysis writeback");
|
|
2804
|
+
}
|
|
2805
|
+
const allTasks = [...await source.list()];
|
|
2806
|
+
const issues = analyzeAll ? allTasks.slice(0, Math.max(1, Math.min(25, Number(body.limit ?? 25) || 25))) : (await Promise.all(ids.map(async (id) => {
|
|
2807
|
+
const cached = allTasks.find((task) => taskRecordId(task) === id);
|
|
2808
|
+
if (cached)
|
|
2809
|
+
return cached;
|
|
2810
|
+
return typeof source.get === "function" ? await source.get(id) : undefined;
|
|
2811
|
+
}))).filter((task) => Boolean(task));
|
|
2812
|
+
if (issues.length === 0) {
|
|
2813
|
+
return deps.jsonResponse({ ok: false, error: "No matching issues found for issue analysis", ids }, 404);
|
|
2814
|
+
}
|
|
2815
|
+
const config = ctx?.config && typeof ctx.config === "object" ? ctx.config : {};
|
|
2816
|
+
const issueAnalysis = config.issueAnalysis && typeof config.issueAnalysis === "object" ? config.issueAnalysis : {};
|
|
2817
|
+
const runtime = config.runtime && typeof config.runtime === "object" ? config.runtime : {};
|
|
2818
|
+
const model = normalizeString(issueAnalysis.model) ?? normalizeString(runtime.model);
|
|
2819
|
+
const service = createIssueAnalysisService({
|
|
2820
|
+
analyzer: createPiIssueAnalyzer({
|
|
2821
|
+
...model ? { model } : {},
|
|
2822
|
+
env: { RIG_PROJECT_ROOT: state.projectRoot }
|
|
2823
|
+
}),
|
|
2824
|
+
writeBack: createIssueAnalysisWriteBack({ target })
|
|
2825
|
+
});
|
|
2826
|
+
const reason = normalizeString(body.reason) ?? "http-issue-analysis";
|
|
2827
|
+
let results;
|
|
2828
|
+
try {
|
|
2829
|
+
results = await service.analyze(issues, { reason, neighbors: ids.length > 0 ? issues : allTasks });
|
|
2830
|
+
} catch (error) {
|
|
2831
|
+
return deps.jsonResponse({
|
|
2832
|
+
ok: false,
|
|
2833
|
+
error: `Issue analysis failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2834
|
+
reason,
|
|
2835
|
+
ids: issues.map((issue) => issue.id)
|
|
2836
|
+
}, 502);
|
|
2837
|
+
}
|
|
2838
|
+
deps.snapshotService.invalidate("issue-analysis-http-run");
|
|
2839
|
+
await state.taskProjectionReconciler?.tick("issue-analysis-http-run").catch(() => {
|
|
2840
|
+
return;
|
|
2841
|
+
});
|
|
2842
|
+
deps.broadcastSnapshotInvalidation(state, "issue-analysis-http-run");
|
|
2843
|
+
return deps.jsonResponse({
|
|
2844
|
+
ok: true,
|
|
2845
|
+
reason,
|
|
2846
|
+
analyzed: results.map((entry) => ({
|
|
2847
|
+
id: entry.issue.id,
|
|
2848
|
+
title: entry.issue.title ?? null,
|
|
2849
|
+
result: entry.result
|
|
2850
|
+
}))
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2520
2853
|
if (url.pathname === "/api/server/status") {
|
|
2521
2854
|
const config = buildProjectConfigStatus(state.projectRoot);
|
|
2522
2855
|
const taskSource = await buildTaskSourceStatus(state, config);
|
|
@@ -3212,11 +3545,12 @@ data: ${JSON.stringify({ connectedAt: new Date().toISOString() })}
|
|
|
3212
3545
|
const runId = normalizeString(body.runId);
|
|
3213
3546
|
const createdAt = normalizeString(body.createdAt) ?? new Date().toISOString();
|
|
3214
3547
|
const promptOverride = normalizeString(body.promptOverride);
|
|
3548
|
+
const restart = body.restart === true;
|
|
3215
3549
|
if (!runId) {
|
|
3216
3550
|
return deps.badRequest("runId is required");
|
|
3217
3551
|
}
|
|
3218
3552
|
try {
|
|
3219
|
-
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride });
|
|
3553
|
+
await deps.resumeRunRecord(state, { runId, createdAt, promptOverride, restart });
|
|
3220
3554
|
deps.broadcastSnapshotInvalidation(state);
|
|
3221
3555
|
return deps.jsonResponse({ ok: true, runId, createdAt });
|
|
3222
3556
|
} catch (error) {
|
|
@@ -151,7 +151,10 @@ function createPiIssueAnalyzer(input = {}) {
|
|
|
151
151
|
const runCommand = input.runCommand ?? createDefaultPiIssueAnalysisCommandRunner();
|
|
152
152
|
return async ({ prompt }) => {
|
|
153
153
|
const args = ["--print", "--mode", "json", "--no-session"];
|
|
154
|
-
const
|
|
154
|
+
const provider = input.provider?.trim() || process.env.RIG_ISSUE_ANALYSIS_PROVIDER?.trim() || process.env.RIG_PI_PROVIDER?.trim();
|
|
155
|
+
const model = input.model?.trim() || process.env.RIG_ISSUE_ANALYSIS_MODEL?.trim() || process.env.RIG_PI_MODEL?.trim() || "openai-codex/gpt-5.5";
|
|
156
|
+
if (provider)
|
|
157
|
+
args.push("--provider", provider);
|
|
155
158
|
if (model)
|
|
156
159
|
args.push("--model", model);
|
|
157
160
|
args.push(prompt);
|