@h-rig/runtime 0.0.6-alpha.0
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/README.md +27 -0
- package/dist/bin/rig-agent-dispatch.js +9615 -0
- package/dist/bin/rig-agent.js +9512 -0
- package/dist/bin/rig-browser-tool.js +269 -0
- package/dist/src/agent-mode.js +48 -0
- package/dist/src/baked-secrets.js +121 -0
- package/dist/src/binary-build-worker.js +312 -0
- package/dist/src/binary-run.js +540 -0
- package/dist/src/boundaries.js +1 -0
- package/dist/src/build-time-config.js +25 -0
- package/dist/src/control-plane/agent-roles.js +27 -0
- package/dist/src/control-plane/agent-wrapper.js +9621 -0
- package/dist/src/control-plane/authority-files.js +582 -0
- package/dist/src/control-plane/browser-contract.js +135 -0
- package/dist/src/control-plane/controlled-bash.js +1111 -0
- package/dist/src/control-plane/errors.js +13 -0
- package/dist/src/control-plane/harness-main.js +10828 -0
- package/dist/src/control-plane/hook-materializer.js +75 -0
- package/dist/src/control-plane/hooks/audit-trail.js +353 -0
- package/dist/src/control-plane/hooks/completion-verification.js +7552 -0
- package/dist/src/control-plane/hooks/import-guard.js +890 -0
- package/dist/src/control-plane/hooks/inject-context.js +4189 -0
- package/dist/src/control-plane/hooks/post-edit-lint.js +43 -0
- package/dist/src/control-plane/hooks/safety-guard.js +910 -0
- package/dist/src/control-plane/hooks/scope-guard.js +907 -0
- package/dist/src/control-plane/hooks/shared.js +44 -0
- package/dist/src/control-plane/hooks/submodule-branch.js +7797 -0
- package/dist/src/control-plane/hooks/task-runtime-start.js +7799 -0
- package/dist/src/control-plane/hooks/test-integrity-guard.js +891 -0
- package/dist/src/control-plane/materialize-task-config.js +453 -0
- package/dist/src/control-plane/memory-sync/cli.js +2019 -0
- package/dist/src/control-plane/memory-sync/db.js +753 -0
- package/dist/src/control-plane/memory-sync/embed.js +281 -0
- package/dist/src/control-plane/memory-sync/index.js +2049 -0
- package/dist/src/control-plane/memory-sync/query.js +294 -0
- package/dist/src/control-plane/memory-sync/read.js +784 -0
- package/dist/src/control-plane/memory-sync/types.js +6 -0
- package/dist/src/control-plane/memory-sync/write.js +1547 -0
- package/dist/src/control-plane/native/git-native.js +490 -0
- package/dist/src/control-plane/native/git-ops.js +2860 -0
- package/dist/src/control-plane/native/harness-cli.js +9721 -0
- package/dist/src/control-plane/native/pr-automation.js +373 -0
- package/dist/src/control-plane/native/profile-ops.js +481 -0
- package/dist/src/control-plane/native/repo-ops.js +2342 -0
- package/dist/src/control-plane/native/root-resolver.js +66 -0
- package/dist/src/control-plane/native/run-ops.js +3281 -0
- package/dist/src/control-plane/native/runtime-native-sidecar.js +299 -0
- package/dist/src/control-plane/native/runtime-native.js +392 -0
- package/dist/src/control-plane/native/scope-rules.js +17 -0
- package/dist/src/control-plane/native/task-ops.js +6320 -0
- package/dist/src/control-plane/native/task-state.js +1512 -0
- package/dist/src/control-plane/native/utils.js +535 -0
- package/dist/src/control-plane/native/validator-binaries.js +889 -0
- package/dist/src/control-plane/native/validator.js +2197 -0
- package/dist/src/control-plane/native/verifier.js +3249 -0
- package/dist/src/control-plane/native/workspace-ops.js +1635 -0
- package/dist/src/control-plane/plugin-host-context.js +334 -0
- package/dist/src/control-plane/project-main-pre-run-sync.js +630 -0
- package/dist/src/control-plane/provider/claude-stream-records.js +158 -0
- package/dist/src/control-plane/provider/codex-app-server.js +885 -0
- package/dist/src/control-plane/provider/codex-exec-records.js +203 -0
- package/dist/src/control-plane/provider/rig-task-run-skill.js +39 -0
- package/dist/src/control-plane/provider/runtime-instructions.js +96 -0
- package/dist/src/control-plane/remote.js +854 -0
- package/dist/src/control-plane/repos/index.js +473 -0
- package/dist/src/control-plane/repos/layout.js +124 -0
- package/dist/src/control-plane/repos/mirror/bootstrap.js +268 -0
- package/dist/src/control-plane/repos/mirror/refresh.js +398 -0
- package/dist/src/control-plane/repos/mirror/state.js +167 -0
- package/dist/src/control-plane/repos/registry.js +77 -0
- package/dist/src/control-plane/repos/types.js +1 -0
- package/dist/src/control-plane/runtime/agent-mode.js +48 -0
- package/dist/src/control-plane/runtime/baked-secrets.js +120 -0
- package/dist/src/control-plane/runtime/claude-tool-router-binary.js +343 -0
- package/dist/src/control-plane/runtime/claude-tool-router.js +520 -0
- package/dist/src/control-plane/runtime/context.js +216 -0
- package/dist/src/control-plane/runtime/events.js +218 -0
- package/dist/src/control-plane/runtime/guard-types.js +6 -0
- package/dist/src/control-plane/runtime/guard.js +880 -0
- package/dist/src/control-plane/runtime/image/fingerprint-sidecar.js +1194 -0
- package/dist/src/control-plane/runtime/image/index.js +2255 -0
- package/dist/src/control-plane/runtime/image-fingerprint-sidecar.js +1191 -0
- package/dist/src/control-plane/runtime/image.js +2255 -0
- package/dist/src/control-plane/runtime/index.js +8511 -0
- package/dist/src/control-plane/runtime/isolation/discovery.js +599 -0
- package/dist/src/control-plane/runtime/isolation/home.js +1217 -0
- package/dist/src/control-plane/runtime/isolation/index.js +8193 -0
- package/dist/src/control-plane/runtime/isolation/runner.js +2651 -0
- package/dist/src/control-plane/runtime/isolation/shared.js +501 -0
- package/dist/src/control-plane/runtime/isolation/toolchain.js +1892 -0
- package/dist/src/control-plane/runtime/isolation/types.js +1 -0
- package/dist/src/control-plane/runtime/isolation/worktree.js +509 -0
- package/dist/src/control-plane/runtime/isolation.js +8193 -0
- package/dist/src/control-plane/runtime/overlay.js +67 -0
- package/dist/src/control-plane/runtime/plugin-mode.js +41 -0
- package/dist/src/control-plane/runtime/plugins.js +1131 -0
- package/dist/src/control-plane/runtime/provisioning-env.js +220 -0
- package/dist/src/control-plane/runtime/queue.js +8358 -0
- package/dist/src/control-plane/runtime/rig-shell.js +205 -0
- package/dist/src/control-plane/runtime/rig-tools.js +182 -0
- package/dist/src/control-plane/runtime/runner-context.js +1 -0
- package/dist/src/control-plane/runtime/runtime-paths.js +184 -0
- package/dist/src/control-plane/runtime/sandbox/backend-bwrap.js +311 -0
- package/dist/src/control-plane/runtime/sandbox/backend-none.js +21 -0
- package/dist/src/control-plane/runtime/sandbox/backend-seatbelt.js +268 -0
- package/dist/src/control-plane/runtime/sandbox/backend.js +1718 -0
- package/dist/src/control-plane/runtime/sandbox/orchestrator.js +1745 -0
- package/dist/src/control-plane/runtime/sandbox/utils.js +137 -0
- package/dist/src/control-plane/runtime/sandbox-backend-bwrap.js +311 -0
- package/dist/src/control-plane/runtime/sandbox-backend-none.js +21 -0
- package/dist/src/control-plane/runtime/sandbox-backend-seatbelt.js +268 -0
- package/dist/src/control-plane/runtime/sandbox-backend.js +1718 -0
- package/dist/src/control-plane/runtime/sandbox-orchestrator.js +1745 -0
- package/dist/src/control-plane/runtime/sandbox-utils.js +137 -0
- package/dist/src/control-plane/runtime/snapshot/index.js +454 -0
- package/dist/src/control-plane/runtime/snapshot/sidecar.js +502 -0
- package/dist/src/control-plane/runtime/snapshot/task-run.js +1578 -0
- package/dist/src/control-plane/runtime/snapshot-sidecar.js +498 -0
- package/dist/src/control-plane/runtime/snapshot.js +454 -0
- package/dist/src/control-plane/runtime/task-run-snapshot.js +1578 -0
- package/dist/src/control-plane/runtime/tool-gateway.js +422 -0
- package/dist/src/control-plane/runtime/tooling/browser-tools.js +32 -0
- package/dist/src/control-plane/runtime/tooling/claude-router-binary.js +343 -0
- package/dist/src/control-plane/runtime/tooling/claude-router.js +524 -0
- package/dist/src/control-plane/runtime/tooling/file-tools.js +182 -0
- package/dist/src/control-plane/runtime/tooling/gateway.js +422 -0
- package/dist/src/control-plane/runtime/tooling/index.js +1290 -0
- package/dist/src/control-plane/runtime/tooling/shell.js +205 -0
- package/dist/src/control-plane/runtime/types.js +1 -0
- package/dist/src/control-plane/setup-version.js +14 -0
- package/dist/src/control-plane/state-sync/index.js +1509 -0
- package/dist/src/control-plane/state-sync/read.js +856 -0
- package/dist/src/control-plane/state-sync/reconcile.js +260 -0
- package/dist/src/control-plane/state-sync/repo.js +302 -0
- package/dist/src/control-plane/state-sync/types.js +111 -0
- package/dist/src/control-plane/state-sync/write.js +1469 -0
- package/dist/src/control-plane/task-fields.js +38 -0
- package/dist/src/control-plane/task-source-bootstrap.js +46 -0
- package/dist/src/control-plane/task-source.js +30 -0
- package/dist/src/control-plane/tasks/legacy-task-config-source.js +130 -0
- package/dist/src/control-plane/tasks/plugin-task-source.js +103 -0
- package/dist/src/control-plane/tasks/source-aware-task-config-source.js +611 -0
- package/dist/src/control-plane/tasks/source-lifecycle.js +1093 -0
- package/dist/src/control-plane/tasks/task-record-reader.js +9 -0
- package/dist/src/control-plane/validators/boundary/public-apis.js +107 -0
- package/dist/src/control-plane/validators/integration/_shared.js +51 -0
- package/dist/src/control-plane/validators/integration/adm-audit-http.js +85 -0
- package/dist/src/control-plane/validators/integration/adm-auth-http.js +78 -0
- package/dist/src/control-plane/validators/integration/adm-issuer-http.js +80 -0
- package/dist/src/control-plane/validators/integration/adm-migration.js +78 -0
- package/dist/src/control-plane/validators/integration/adm-scaffold.js +78 -0
- package/dist/src/control-plane/validators/runtime-registration.js +64 -0
- package/dist/src/control-plane/validators/shared.js +683 -0
- package/dist/src/events.js +218 -0
- package/dist/src/execution.js +35 -0
- package/dist/src/index.js +1633 -0
- package/dist/src/layout.js +145 -0
- package/dist/src/local-server.js +202 -0
- package/dist/src/plugins.js +329 -0
- package/dist/src/remote-http.js +83 -0
- package/dist/src/runtime-context.js +216 -0
- package/dist/src/types.js +1 -0
- package/native/darwin-arm64/bin/rig-git +0 -0
- package/native/darwin-arm64/bin/rig-shell +0 -0
- package/native/darwin-arm64/bin/rig-tools +0 -0
- package/native/darwin-arm64/lib/runtime-native-darwin-arm64.dylib +0 -0
- package/native/darwin-arm64/lib/runtime-native.dylib +0 -0
- package/native/darwin-arm64/manifest.json +1 -0
- package/native/linux-x64/bin/rig-git +0 -0
- package/native/linux-x64/bin/rig-shell +0 -0
- package/native/linux-x64/bin/rig-tools +0 -0
- package/native/linux-x64/lib/runtime-native-linux-x64.so +0 -0
- package/native/linux-x64/lib/runtime-native.so +0 -0
- package/native/linux-x64/manifest.json +1 -0
- package/package.json +74 -0
- package/skills/rig-task-run.md +71 -0
|
@@ -0,0 +1,3249 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
3
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
4
|
+
import { resolve as resolve13 } from "path";
|
|
5
|
+
|
|
6
|
+
// packages/runtime/src/control-plane/runtime/baked-secrets.ts
|
|
7
|
+
var BAKED_RUNTIME_SECRETS = {
|
|
8
|
+
ANTHROPIC_API_KEY: typeof RIG_BAKED_ANTHROPIC_API_KEY !== "undefined" ? RIG_BAKED_ANTHROPIC_API_KEY : "",
|
|
9
|
+
OPENAI_API_KEY: typeof RIG_BAKED_OPENAI_API_KEY !== "undefined" ? RIG_BAKED_OPENAI_API_KEY : "",
|
|
10
|
+
OPENROUTER_API_KEY: typeof RIG_BAKED_OPENROUTER_API_KEY !== "undefined" ? RIG_BAKED_OPENROUTER_API_KEY : "",
|
|
11
|
+
AI_REVIEW_MODE: typeof RIG_BAKED_AI_REVIEW_MODE !== "undefined" ? RIG_BAKED_AI_REVIEW_MODE : "",
|
|
12
|
+
AI_REVIEW_PROVIDER: typeof RIG_BAKED_AI_REVIEW_PROVIDER !== "undefined" ? RIG_BAKED_AI_REVIEW_PROVIDER : "",
|
|
13
|
+
GREPTILE_API_BASE: typeof RIG_BAKED_GREPTILE_API_BASE !== "undefined" ? RIG_BAKED_GREPTILE_API_BASE : "",
|
|
14
|
+
GREPTILE_REMOTE: typeof RIG_BAKED_GREPTILE_REMOTE !== "undefined" ? RIG_BAKED_GREPTILE_REMOTE : "",
|
|
15
|
+
GREPTILE_REPOSITORY: typeof RIG_BAKED_GREPTILE_REPOSITORY !== "undefined" ? RIG_BAKED_GREPTILE_REPOSITORY : "",
|
|
16
|
+
GREPTILE_CONTEXT_BRANCH: typeof RIG_BAKED_GREPTILE_CONTEXT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_CONTEXT_BRANCH : "",
|
|
17
|
+
GREPTILE_DEFAULT_BRANCH: typeof RIG_BAKED_GREPTILE_DEFAULT_BRANCH !== "undefined" ? RIG_BAKED_GREPTILE_DEFAULT_BRANCH : "",
|
|
18
|
+
GREPTILE_API_KEY: typeof RIG_BAKED_GREPTILE_API_KEY !== "undefined" ? RIG_BAKED_GREPTILE_API_KEY : "",
|
|
19
|
+
GREPTILE_GITHUB_TOKEN: typeof RIG_BAKED_GREPTILE_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GREPTILE_GITHUB_TOKEN : "",
|
|
20
|
+
GREPTILE_POLL_ATTEMPTS: typeof RIG_BAKED_GREPTILE_POLL_ATTEMPTS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_ATTEMPTS : "",
|
|
21
|
+
GREPTILE_POLL_INTERVAL_MS: typeof RIG_BAKED_GREPTILE_POLL_INTERVAL_MS !== "undefined" ? RIG_BAKED_GREPTILE_POLL_INTERVAL_MS : "",
|
|
22
|
+
GH_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
23
|
+
GITHUB_TOKEN: typeof RIG_BAKED_GITHUB_TOKEN !== "undefined" ? RIG_BAKED_GITHUB_TOKEN : "",
|
|
24
|
+
GITHUB_SSH_KEY: typeof RIG_BAKED_GITHUB_SSH_KEY !== "undefined" ? RIG_BAKED_GITHUB_SSH_KEY : "",
|
|
25
|
+
AWS_ACCESS_KEY_ID: typeof RIG_BAKED_AWS_ACCESS_KEY_ID !== "undefined" ? RIG_BAKED_AWS_ACCESS_KEY_ID : "",
|
|
26
|
+
AWS_SECRET_ACCESS_KEY: typeof RIG_BAKED_AWS_SECRET_ACCESS_KEY !== "undefined" ? RIG_BAKED_AWS_SECRET_ACCESS_KEY : "",
|
|
27
|
+
AWS_REGION: typeof RIG_BAKED_AWS_REGION !== "undefined" ? RIG_BAKED_AWS_REGION : "",
|
|
28
|
+
LINEAR_API_KEY: typeof RIG_BAKED_LINEAR_API_KEY !== "undefined" ? RIG_BAKED_LINEAR_API_KEY : "",
|
|
29
|
+
LINEAR_WEBHOOK_SECRET: typeof RIG_BAKED_LINEAR_WEBHOOK_SECRET !== "undefined" ? RIG_BAKED_LINEAR_WEBHOOK_SECRET : ""
|
|
30
|
+
};
|
|
31
|
+
function resolveRuntimeSecrets(env, baked = BAKED_RUNTIME_SECRETS) {
|
|
32
|
+
const resolved = {};
|
|
33
|
+
const keys = new Set([
|
|
34
|
+
...Object.keys(BAKED_RUNTIME_SECRETS),
|
|
35
|
+
...Object.keys(baked)
|
|
36
|
+
]);
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const envValue = env[key]?.trim();
|
|
39
|
+
const bakedValue = baked[key]?.trim();
|
|
40
|
+
if (envValue) {
|
|
41
|
+
resolved[key] = envValue;
|
|
42
|
+
} else if (bakedValue) {
|
|
43
|
+
resolved[key] = bakedValue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
50
|
+
import { existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
51
|
+
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve12 } from "path";
|
|
52
|
+
|
|
53
|
+
// packages/runtime/src/control-plane/runtime/context.ts
|
|
54
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
55
|
+
import { dirname, resolve } from "path";
|
|
56
|
+
var RUNTIME_CONTEXT_ENV = "RIG_RUNTIME_CONTEXT_FILE";
|
|
57
|
+
var runtimeContextStringFields = [
|
|
58
|
+
"runtimeId",
|
|
59
|
+
"taskId",
|
|
60
|
+
"role",
|
|
61
|
+
"workspaceDir",
|
|
62
|
+
"stateDir",
|
|
63
|
+
"logsDir",
|
|
64
|
+
"sessionDir",
|
|
65
|
+
"sessionFile",
|
|
66
|
+
"policyFile",
|
|
67
|
+
"binDir",
|
|
68
|
+
"createdAt"
|
|
69
|
+
];
|
|
70
|
+
var runtimeContextArrayFields = ["scopes", "validation"];
|
|
71
|
+
var runtimeContextOptionalStringFields = [
|
|
72
|
+
"artifactRoot",
|
|
73
|
+
"hostProjectRoot",
|
|
74
|
+
"monorepoMainRoot",
|
|
75
|
+
"monorepoBaseRef",
|
|
76
|
+
"monorepoBaseCommit"
|
|
77
|
+
];
|
|
78
|
+
function loadRuntimeContext(path) {
|
|
79
|
+
const absPath = resolve(path);
|
|
80
|
+
if (!existsSync(absPath)) {
|
|
81
|
+
throw new Error(`RuntimeTaskContext file not found: ${absPath}`);
|
|
82
|
+
}
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = JSON.parse(readFileSync(absPath, "utf8"));
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new Error(`Failed to parse RuntimeTaskContext at ${absPath}: ${String(err)}`);
|
|
88
|
+
}
|
|
89
|
+
if (typeof raw !== "object" || raw === null) {
|
|
90
|
+
throw new Error(`RuntimeTaskContext at ${absPath} is not an object`);
|
|
91
|
+
}
|
|
92
|
+
const obj = raw;
|
|
93
|
+
for (const field of runtimeContextStringFields) {
|
|
94
|
+
if (typeof obj[field] !== "string" || obj[field].length === 0) {
|
|
95
|
+
throw new Error(`RuntimeTaskContext field "${field}" must be a non-empty string (at ${absPath})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const field of runtimeContextArrayFields) {
|
|
99
|
+
if (!Array.isArray(obj[field])) {
|
|
100
|
+
throw new Error(`RuntimeTaskContext field "${field}" must be an array (at ${absPath})`);
|
|
101
|
+
}
|
|
102
|
+
if (!obj[field].every((entry) => typeof entry === "string")) {
|
|
103
|
+
throw new Error(`RuntimeTaskContext field "${field}" must be a string[] (at ${absPath})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const field of runtimeContextOptionalStringFields) {
|
|
107
|
+
if (field in obj && obj[field] !== undefined && typeof obj[field] !== "string") {
|
|
108
|
+
throw new Error(`RuntimeTaskContext field "${field}" must be a string when present (at ${absPath})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (obj.browser !== undefined) {
|
|
112
|
+
if (typeof obj.browser !== "object" || obj.browser === null || Array.isArray(obj.browser)) {
|
|
113
|
+
throw new Error(`RuntimeTaskContext field "browser" must be an object when present (at ${absPath})`);
|
|
114
|
+
}
|
|
115
|
+
const browser = obj.browser;
|
|
116
|
+
for (const field of [
|
|
117
|
+
"preset",
|
|
118
|
+
"mode",
|
|
119
|
+
"stateDir",
|
|
120
|
+
"defaultProfile",
|
|
121
|
+
"effectiveProfile",
|
|
122
|
+
"defaultAttachUrl",
|
|
123
|
+
"effectiveAttachUrl",
|
|
124
|
+
"launchHelper",
|
|
125
|
+
"checkHelper",
|
|
126
|
+
"attachInfoHelper",
|
|
127
|
+
"e2eHelper",
|
|
128
|
+
"resetProfileHelper"
|
|
129
|
+
]) {
|
|
130
|
+
if (typeof browser[field] !== "string" || browser[field].length === 0) {
|
|
131
|
+
throw new Error(`RuntimeTaskContext field "browser.${field}" must be a non-empty string (at ${absPath})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const field of ["devCommand", "launchCommand", "checkCommand", "e2eCommand"]) {
|
|
135
|
+
if (browser[field] !== undefined && typeof browser[field] !== "string") {
|
|
136
|
+
throw new Error(`RuntimeTaskContext field "browser.${field}" must be a string when present (at ${absPath})`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (typeof browser.required !== "boolean") {
|
|
140
|
+
throw new Error(`RuntimeTaskContext field "browser.required" must be a boolean (at ${absPath})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (obj.memory !== undefined) {
|
|
144
|
+
if (typeof obj.memory !== "object" || obj.memory === null || Array.isArray(obj.memory)) {
|
|
145
|
+
throw new Error(`RuntimeTaskContext field "memory" must be an object when present (at ${absPath})`);
|
|
146
|
+
}
|
|
147
|
+
const memory = obj.memory;
|
|
148
|
+
for (const field of ["canonicalPath", "canonicalRef", "canonicalBaseOid", "hydratedPath"]) {
|
|
149
|
+
if (typeof memory[field] !== "string" || memory[field].length === 0) {
|
|
150
|
+
throw new Error(`RuntimeTaskContext field "memory.${field}" must be a non-empty string (at ${absPath})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (typeof memory.createdFresh !== "boolean") {
|
|
154
|
+
throw new Error(`RuntimeTaskContext field "memory.createdFresh" must be a boolean (at ${absPath})`);
|
|
155
|
+
}
|
|
156
|
+
if (typeof memory.retrieval !== "object" || memory.retrieval === null || Array.isArray(memory.retrieval)) {
|
|
157
|
+
throw new Error(`RuntimeTaskContext field "memory.retrieval" must be an object (at ${absPath})`);
|
|
158
|
+
}
|
|
159
|
+
const retrieval = memory.retrieval;
|
|
160
|
+
for (const field of ["topK", "lexicalWeight", "vectorWeight", "recencyWeight", "confidenceWeight"]) {
|
|
161
|
+
if (typeof retrieval[field] !== "number" || Number.isNaN(retrieval[field])) {
|
|
162
|
+
throw new Error(`RuntimeTaskContext field "memory.retrieval.${field}" must be a number (at ${absPath})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (obj.initialDirtyFiles !== undefined) {
|
|
167
|
+
if (typeof obj.initialDirtyFiles !== "object" || obj.initialDirtyFiles === null || Array.isArray(obj.initialDirtyFiles)) {
|
|
168
|
+
throw new Error(`RuntimeTaskContext field "initialDirtyFiles" must be an object when present (at ${absPath})`);
|
|
169
|
+
}
|
|
170
|
+
const dirtyFiles = obj.initialDirtyFiles;
|
|
171
|
+
for (const key of ["project", "monorepo"]) {
|
|
172
|
+
if (dirtyFiles[key] === undefined) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!Array.isArray(dirtyFiles[key]) || !dirtyFiles[key].every((entry) => typeof entry === "string")) {
|
|
176
|
+
throw new Error(`RuntimeTaskContext field "initialDirtyFiles.${key}" must be a string[] when present (at ${absPath})`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (obj.initialHeadCommits !== undefined) {
|
|
181
|
+
if (typeof obj.initialHeadCommits !== "object" || obj.initialHeadCommits === null || Array.isArray(obj.initialHeadCommits)) {
|
|
182
|
+
throw new Error(`RuntimeTaskContext field "initialHeadCommits" must be an object when present (at ${absPath})`);
|
|
183
|
+
}
|
|
184
|
+
const headCommits = obj.initialHeadCommits;
|
|
185
|
+
for (const key of ["project", "monorepo"]) {
|
|
186
|
+
if (headCommits[key] === undefined) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (typeof headCommits[key] !== "string") {
|
|
190
|
+
throw new Error(`RuntimeTaskContext field "initialHeadCommits.${key}" must be a string when present (at ${absPath})`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return obj;
|
|
195
|
+
}
|
|
196
|
+
function loadRuntimeContextFromEnv(env = process.env) {
|
|
197
|
+
const contextFile = env[RUNTIME_CONTEXT_ENV];
|
|
198
|
+
if (contextFile) {
|
|
199
|
+
return loadRuntimeContext(contextFile);
|
|
200
|
+
}
|
|
201
|
+
const inferred = findRuntimeContextFile(process.cwd());
|
|
202
|
+
if (!inferred) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return loadRuntimeContext(inferred);
|
|
206
|
+
}
|
|
207
|
+
function findRuntimeContextFile(startPath) {
|
|
208
|
+
let current = resolve(startPath);
|
|
209
|
+
while (true) {
|
|
210
|
+
const candidate = resolve(current, "runtime-context.json");
|
|
211
|
+
if (existsSync(candidate) && isAgentRuntimeContextPath(candidate)) {
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
const parent = dirname(current);
|
|
215
|
+
if (parent === current) {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
current = parent;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function isAgentRuntimeContextPath(path) {
|
|
222
|
+
const normalized = path.replace(/\\/g, "/");
|
|
223
|
+
return /\/\.rig\/runtime-context\.json$/.test(normalized);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// packages/runtime/src/build-time-config.ts
|
|
227
|
+
function normalizeBuildConfig(value) {
|
|
228
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
229
|
+
return {};
|
|
230
|
+
}
|
|
231
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
232
|
+
}
|
|
233
|
+
function readBuildConfig() {
|
|
234
|
+
if (typeof __RIG_BUILD_CONFIG__ !== "undefined") {
|
|
235
|
+
return normalizeBuildConfig(__RIG_BUILD_CONFIG__);
|
|
236
|
+
}
|
|
237
|
+
const raw = process.env.RIG_BUILD_CONFIG_JSON?.trim();
|
|
238
|
+
if (!raw) {
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
return normalizeBuildConfig(JSON.parse(raw));
|
|
243
|
+
} catch {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// packages/runtime/src/control-plane/runtime/tooling/shell.ts
|
|
249
|
+
import { tmpdir } from "os";
|
|
250
|
+
import { basename, dirname as dirname2, resolve as resolve2 } from "path";
|
|
251
|
+
var sharedNativeShellOutputDir = resolve2(tmpdir(), "rig-native");
|
|
252
|
+
var sharedNativeShellOutputPath = resolve2(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
253
|
+
// packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
|
|
254
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
255
|
+
import { basename as basename2, dirname as dirname3, resolve as resolve3 } from "path";
|
|
256
|
+
var sharedNativeToolsOutputDir = resolve3(tmpdir2(), "rig-native");
|
|
257
|
+
var sharedNativeToolsOutputPath = resolve3(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
258
|
+
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
259
|
+
import { createPluginHost } from "@rig/core";
|
|
260
|
+
import { loadConfig } from "@rig/core/load-config";
|
|
261
|
+
|
|
262
|
+
// packages/runtime/src/control-plane/task-source.ts
|
|
263
|
+
function createTaskSourceRegistry() {
|
|
264
|
+
const byId = new Map;
|
|
265
|
+
const order = [];
|
|
266
|
+
return {
|
|
267
|
+
register(s) {
|
|
268
|
+
if (byId.has(s.id))
|
|
269
|
+
throw new Error(`task source already registered: ${s.id}`);
|
|
270
|
+
byId.set(s.id, s);
|
|
271
|
+
order.push(s);
|
|
272
|
+
},
|
|
273
|
+
resolveById(id) {
|
|
274
|
+
const s = byId.get(id);
|
|
275
|
+
if (!s)
|
|
276
|
+
throw new Error(`task source not registered: ${id}`);
|
|
277
|
+
return s;
|
|
278
|
+
},
|
|
279
|
+
resolveByKind(kind) {
|
|
280
|
+
for (const s of order)
|
|
281
|
+
if (s.kind === kind)
|
|
282
|
+
return s;
|
|
283
|
+
throw new Error(`no task source registered for kind: ${kind}`);
|
|
284
|
+
},
|
|
285
|
+
list: () => order
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// packages/runtime/src/control-plane/task-source-bootstrap.ts
|
|
290
|
+
function formatRegisteredKinds(pluginHost) {
|
|
291
|
+
const kinds = pluginHost ? pluginHost.listExecutableTaskSources().map((source) => source.kind) : [];
|
|
292
|
+
return kinds.length > 0 ? kinds.join(", ") : "none";
|
|
293
|
+
}
|
|
294
|
+
function buildTaskSourceRegistry(config, pluginHost) {
|
|
295
|
+
const registry = createTaskSourceRegistry();
|
|
296
|
+
const taskSourceConfig = config.taskSource;
|
|
297
|
+
const factory = pluginHost?.resolveTaskSourceFactoryByKind(taskSourceConfig.kind);
|
|
298
|
+
if (!factory) {
|
|
299
|
+
throw new Error(`No task source factory registered for kind "${taskSourceConfig.kind}". ` + `Registered kinds: ${formatRegisteredKinds(pluginHost)}. ` + "Load a plugin that contributes an executable task source factory for this kind.");
|
|
300
|
+
}
|
|
301
|
+
registry.register(factory.factory(taskSourceConfig));
|
|
302
|
+
return registry;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// packages/runtime/src/control-plane/repos/registry.ts
|
|
306
|
+
function createRepoRegistry(entries) {
|
|
307
|
+
const map = new Map;
|
|
308
|
+
for (const e of entries) {
|
|
309
|
+
if (map.has(e.id))
|
|
310
|
+
throw new Error(`repo already registered: ${e.id}`);
|
|
311
|
+
map.set(e.id, { ...e });
|
|
312
|
+
}
|
|
313
|
+
const ordered = Array.from(map.values());
|
|
314
|
+
return {
|
|
315
|
+
getById: (id) => map.get(id),
|
|
316
|
+
list: () => ordered
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
var MANAGED_REPOS = new Map;
|
|
320
|
+
function setManagedRepos(entries) {
|
|
321
|
+
const next = new Map;
|
|
322
|
+
for (const e of entries) {
|
|
323
|
+
if (next.has(e.id)) {
|
|
324
|
+
throw new Error(`managed repo already registered: ${e.id}`);
|
|
325
|
+
}
|
|
326
|
+
next.set(e.id, e);
|
|
327
|
+
}
|
|
328
|
+
MANAGED_REPOS = next;
|
|
329
|
+
}
|
|
330
|
+
function repoRegistrationToManagedEntry(reg) {
|
|
331
|
+
if (!reg.defaultBranch) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
id: reg.id,
|
|
336
|
+
alias: reg.defaultPath ?? reg.id,
|
|
337
|
+
defaultBranch: reg.defaultBranch,
|
|
338
|
+
defaultRemoteUrl: reg.url,
|
|
339
|
+
remoteEnvVar: reg.remoteEnvVar,
|
|
340
|
+
checkoutEnvVar: reg.checkoutEnvVar
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// packages/runtime/src/control-plane/agent-roles.ts
|
|
345
|
+
function createAgentRoleRegistry(pluginRoles, configOverrides) {
|
|
346
|
+
const map = new Map;
|
|
347
|
+
for (const r of pluginRoles) {
|
|
348
|
+
if (map.has(r.id))
|
|
349
|
+
throw new Error(`agent role already registered: ${r.id}`);
|
|
350
|
+
const override = configOverrides?.[r.id];
|
|
351
|
+
const model = override?.model ?? r.defaultModel;
|
|
352
|
+
if (!model) {
|
|
353
|
+
throw new Error(`agent role "${r.id}" has no model \u2014 provide defaultModel in plugin or model in config.runtime.agentRoles.${r.id}`);
|
|
354
|
+
}
|
|
355
|
+
map.set(r.id, { ...r, model });
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
resolve(id) {
|
|
359
|
+
const r = map.get(id);
|
|
360
|
+
if (!r)
|
|
361
|
+
throw new Error(`agent role not registered: ${id}`);
|
|
362
|
+
return r;
|
|
363
|
+
},
|
|
364
|
+
list: () => Array.from(map.values())
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// packages/runtime/src/control-plane/task-fields.ts
|
|
369
|
+
function createTaskFieldRegistry(extensions) {
|
|
370
|
+
const byId = new Map;
|
|
371
|
+
for (const e of extensions) {
|
|
372
|
+
if (byId.has(e.id))
|
|
373
|
+
throw new Error(`task field extension already registered: ${e.id}`);
|
|
374
|
+
byId.set(e.id, e);
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
get: (id) => byId.get(id),
|
|
378
|
+
list: () => Array.from(byId.values()),
|
|
379
|
+
fieldNames: () => Array.from(byId.values()).map((e) => e.fieldName),
|
|
380
|
+
validateTaskFields(task) {
|
|
381
|
+
const errors = [];
|
|
382
|
+
for (const ext of byId.values()) {
|
|
383
|
+
let schema;
|
|
384
|
+
try {
|
|
385
|
+
schema = JSON.parse(ext.schemaJson);
|
|
386
|
+
} catch {
|
|
387
|
+
errors.push(`task field "${ext.id}": schemaJson is not valid JSON`);
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const isRequired = typeof schema === "object" && schema !== null && schema.required === true;
|
|
391
|
+
if (!isRequired)
|
|
392
|
+
continue;
|
|
393
|
+
const value = task[ext.fieldName];
|
|
394
|
+
if (value === undefined || value === null || value === "") {
|
|
395
|
+
errors.push(`task field "${ext.fieldName}" (from extension "${ext.id}") is required but missing`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// packages/runtime/src/control-plane/validators/runtime-registration.ts
|
|
404
|
+
import { existsSync as existsSync2 } from "fs";
|
|
405
|
+
import { join } from "path";
|
|
406
|
+
function createValidatorRegistry() {
|
|
407
|
+
const map = new Map;
|
|
408
|
+
const order = [];
|
|
409
|
+
const registry = {
|
|
410
|
+
register(v) {
|
|
411
|
+
if (map.has(v.id))
|
|
412
|
+
throw new Error(`validator already registered: ${v.id}`);
|
|
413
|
+
map.set(v.id, v);
|
|
414
|
+
order.push(v);
|
|
415
|
+
},
|
|
416
|
+
resolve(id) {
|
|
417
|
+
const v = map.get(id);
|
|
418
|
+
if (!v)
|
|
419
|
+
throw new Error(`validator not registered: ${id}`);
|
|
420
|
+
return v;
|
|
421
|
+
},
|
|
422
|
+
list: () => order
|
|
423
|
+
};
|
|
424
|
+
registerBuiltInValidators(registry);
|
|
425
|
+
return registry;
|
|
426
|
+
}
|
|
427
|
+
function registerBuiltInValidators(registry) {
|
|
428
|
+
registry.register({
|
|
429
|
+
id: "std:typecheck",
|
|
430
|
+
category: "custom",
|
|
431
|
+
description: "Runs the package typecheck script when present.",
|
|
432
|
+
run: async (ctx) => runStdTypecheck(ctx)
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async function runStdTypecheck(ctx) {
|
|
436
|
+
const packageJsonPath = join(ctx.workspaceRoot, "package.json");
|
|
437
|
+
if (!existsSync2(packageJsonPath)) {
|
|
438
|
+
return {
|
|
439
|
+
id: "std:typecheck",
|
|
440
|
+
passed: false,
|
|
441
|
+
summary: `package.json not found at ${packageJsonPath}`
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const proc = Bun.spawn(["bun", "run", "typecheck"], {
|
|
445
|
+
cwd: ctx.workspaceRoot,
|
|
446
|
+
env: process.env,
|
|
447
|
+
stdout: "pipe",
|
|
448
|
+
stderr: "pipe"
|
|
449
|
+
});
|
|
450
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
451
|
+
proc.exited,
|
|
452
|
+
new Response(proc.stdout).text(),
|
|
453
|
+
new Response(proc.stderr).text()
|
|
454
|
+
]);
|
|
455
|
+
const output = `${stdout}${stderr}`.trim();
|
|
456
|
+
return {
|
|
457
|
+
id: "std:typecheck",
|
|
458
|
+
passed: exitCode === 0,
|
|
459
|
+
summary: exitCode === 0 ? "typecheck passed" : "typecheck failed",
|
|
460
|
+
...output ? { details: output.slice(0, 4000) } : {}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// packages/runtime/src/control-plane/native/scope-rules.ts
|
|
465
|
+
var activeRules = null;
|
|
466
|
+
function setScopeRules(rules) {
|
|
467
|
+
activeRules = rules ?? null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// packages/runtime/src/control-plane/hook-materializer.ts
|
|
471
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
472
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
473
|
+
var MARKER_PLUGIN = "_rigPlugin";
|
|
474
|
+
var MARKER_HOOK_ID = "_rigHookId";
|
|
475
|
+
function matcherToString(matcher) {
|
|
476
|
+
if (matcher.kind === "all")
|
|
477
|
+
return;
|
|
478
|
+
if (matcher.kind === "tool")
|
|
479
|
+
return matcher.name;
|
|
480
|
+
return matcher.pattern;
|
|
481
|
+
}
|
|
482
|
+
function isPluginOwned(cmd) {
|
|
483
|
+
return typeof cmd[MARKER_PLUGIN] === "string";
|
|
484
|
+
}
|
|
485
|
+
function materializeHooks(projectRoot, entries) {
|
|
486
|
+
const settingsPath = resolve4(projectRoot, ".claude", "settings.json");
|
|
487
|
+
const existing = existsSync3(settingsPath) ? safeReadJson(settingsPath) : {};
|
|
488
|
+
const hooks = existing.hooks ?? {};
|
|
489
|
+
for (const event of Object.keys(hooks)) {
|
|
490
|
+
const groups = hooks[event] ?? [];
|
|
491
|
+
const cleaned = [];
|
|
492
|
+
for (const group of groups) {
|
|
493
|
+
const operatorHooks = group.hooks.filter((h) => !isPluginOwned(h));
|
|
494
|
+
if (operatorHooks.length > 0) {
|
|
495
|
+
cleaned.push({ ...group, hooks: operatorHooks });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (cleaned.length > 0) {
|
|
499
|
+
hooks[event] = cleaned;
|
|
500
|
+
} else {
|
|
501
|
+
delete hooks[event];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
for (const { pluginName, hook } of entries) {
|
|
505
|
+
if (!hook.command) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const event = hook.event;
|
|
509
|
+
const matcherString = matcherToString(hook.matcher);
|
|
510
|
+
const groups = hooks[event] ??= [];
|
|
511
|
+
let group = groups.find((g) => g.matcher === matcherString);
|
|
512
|
+
if (!group) {
|
|
513
|
+
group = matcherString === undefined ? { hooks: [] } : { matcher: matcherString, hooks: [] };
|
|
514
|
+
groups.push(group);
|
|
515
|
+
}
|
|
516
|
+
group.hooks.push({
|
|
517
|
+
type: "command",
|
|
518
|
+
command: hook.command,
|
|
519
|
+
[MARKER_PLUGIN]: pluginName,
|
|
520
|
+
[MARKER_HOOK_ID]: hook.id
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const next = { ...existing };
|
|
524
|
+
if (Object.keys(hooks).length > 0) {
|
|
525
|
+
next.hooks = hooks;
|
|
526
|
+
} else {
|
|
527
|
+
delete next.hooks;
|
|
528
|
+
}
|
|
529
|
+
mkdirSync2(dirname4(settingsPath), { recursive: true });
|
|
530
|
+
writeFileSync2(settingsPath, `${JSON.stringify(next, null, 2)}
|
|
531
|
+
`, "utf-8");
|
|
532
|
+
return settingsPath;
|
|
533
|
+
}
|
|
534
|
+
function safeReadJson(path) {
|
|
535
|
+
try {
|
|
536
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
537
|
+
} catch {
|
|
538
|
+
return {};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// packages/runtime/src/control-plane/plugin-host-context.ts
|
|
543
|
+
async function buildPluginHostContext(projectRoot) {
|
|
544
|
+
let config;
|
|
545
|
+
try {
|
|
546
|
+
config = await loadConfig(projectRoot);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
549
|
+
if (msg.includes("no rig.config")) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
throw err;
|
|
553
|
+
}
|
|
554
|
+
const pluginHost = createPluginHost(config.plugins);
|
|
555
|
+
setScopeRules(config.workspace.scopeNormalization);
|
|
556
|
+
const validatorRegistry = createValidatorRegistry();
|
|
557
|
+
for (const impl of pluginHost.listExecutableValidators()) {
|
|
558
|
+
validatorRegistry.register(impl);
|
|
559
|
+
}
|
|
560
|
+
const taskSourceRegistry = buildTaskSourceRegistry(config, pluginHost);
|
|
561
|
+
const repoRegistry = createRepoRegistry(pluginHost.listRepoSources());
|
|
562
|
+
const managedEntries = pluginHost.listRepoSources().map(repoRegistrationToManagedEntry).filter((e) => e !== null);
|
|
563
|
+
setManagedRepos(managedEntries);
|
|
564
|
+
const configRoleOverrides = config.runtime?.agentRoles;
|
|
565
|
+
const agentRoleRegistry = createAgentRoleRegistry(pluginHost.listAgentRoles(), configRoleOverrides);
|
|
566
|
+
const taskFieldRegistry = createTaskFieldRegistry(pluginHost.listTaskFieldExtensions());
|
|
567
|
+
try {
|
|
568
|
+
const hookEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.hooks ?? []).map((hook) => ({
|
|
569
|
+
pluginName: plugin.name,
|
|
570
|
+
hook
|
|
571
|
+
})));
|
|
572
|
+
if (hookEntries.length > 0) {
|
|
573
|
+
materializeHooks(projectRoot, hookEntries);
|
|
574
|
+
}
|
|
575
|
+
} catch (err) {
|
|
576
|
+
console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
config,
|
|
580
|
+
pluginHost,
|
|
581
|
+
validatorRegistry,
|
|
582
|
+
taskSourceRegistry,
|
|
583
|
+
repoRegistry,
|
|
584
|
+
agentRoleRegistry,
|
|
585
|
+
taskFieldRegistry
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
590
|
+
import { spawnSync } from "child_process";
|
|
591
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
592
|
+
import { basename as basename3, join as join2, resolve as resolve6 } from "path";
|
|
593
|
+
|
|
594
|
+
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
595
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
596
|
+
import { resolve as resolve5 } from "path";
|
|
597
|
+
|
|
598
|
+
// packages/runtime/src/control-plane/tasks/task-record-reader.ts
|
|
599
|
+
async function findTaskById(reader, id) {
|
|
600
|
+
const tasks = await reader.listTasks();
|
|
601
|
+
return tasks.find((task) => task.id === id) ?? null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
|
|
605
|
+
class LegacyTaskConfigReadError extends Error {
|
|
606
|
+
code = "LEGACY_TASK_CONFIG_READ_FAILED";
|
|
607
|
+
projectRoot;
|
|
608
|
+
configPath;
|
|
609
|
+
cause;
|
|
610
|
+
constructor(input) {
|
|
611
|
+
super(input.message, { cause: input.cause });
|
|
612
|
+
this.name = "LegacyTaskConfigReadError";
|
|
613
|
+
this.projectRoot = input.projectRoot;
|
|
614
|
+
this.configPath = input.configPath;
|
|
615
|
+
this.cause = input.cause;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
|
|
619
|
+
const configPath = options.configPath ?? resolve5(projectRoot, ".rig", "task-config.json");
|
|
620
|
+
const reader = {
|
|
621
|
+
async listTasks() {
|
|
622
|
+
return readLegacyTaskRecords(projectRoot, configPath);
|
|
623
|
+
},
|
|
624
|
+
async getTask(id) {
|
|
625
|
+
return findTaskById(reader, id);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
return reader;
|
|
629
|
+
}
|
|
630
|
+
function readLegacyTaskRecords(projectRoot, configPath = resolve5(projectRoot, ".rig", "task-config.json")) {
|
|
631
|
+
if (!existsSync4(configPath)) {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
|
|
635
|
+
return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
|
|
636
|
+
}
|
|
637
|
+
function readLegacyTaskConfigJson(projectRoot, configPath) {
|
|
638
|
+
try {
|
|
639
|
+
const parsed = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
640
|
+
if (isPlainRecord(parsed)) {
|
|
641
|
+
return parsed;
|
|
642
|
+
}
|
|
643
|
+
throw new Error("task config root must be a JSON object");
|
|
644
|
+
} catch (cause) {
|
|
645
|
+
throw new LegacyTaskConfigReadError({
|
|
646
|
+
projectRoot,
|
|
647
|
+
configPath,
|
|
648
|
+
message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
649
|
+
cause
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function stripLegacyTaskConfigMetadata(raw) {
|
|
654
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
655
|
+
return tasks;
|
|
656
|
+
}
|
|
657
|
+
function legacyTaskConfigEntryToRecord(id, entry) {
|
|
658
|
+
if (!isPlainRecord(entry)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
|
|
662
|
+
const validation = readStringList(entry.validation);
|
|
663
|
+
const validators = readStringList(entry.validators);
|
|
664
|
+
const scope = readStringList(entry.scope);
|
|
665
|
+
const status = typeof entry.status === "string" ? entry.status : "open";
|
|
666
|
+
const title = typeof entry.title === "string" ? entry.title : undefined;
|
|
667
|
+
const description = typeof entry.description === "string" ? entry.description : undefined;
|
|
668
|
+
const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
|
|
669
|
+
return {
|
|
670
|
+
id,
|
|
671
|
+
deps,
|
|
672
|
+
status,
|
|
673
|
+
source: "legacy-task-config",
|
|
674
|
+
...title ? { title } : {},
|
|
675
|
+
...description ? { description } : {},
|
|
676
|
+
...acceptanceCriteria ? { acceptanceCriteria } : {},
|
|
677
|
+
...scope.length > 0 ? { scope } : {},
|
|
678
|
+
...validation.length > 0 ? { validation } : {},
|
|
679
|
+
...validators.length > 0 ? { validators } : {},
|
|
680
|
+
...preservedLegacyFields(entry)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function preservedLegacyFields(entry) {
|
|
684
|
+
const preserved = {};
|
|
685
|
+
for (const key of [
|
|
686
|
+
"role",
|
|
687
|
+
"browser",
|
|
688
|
+
"repo_pins",
|
|
689
|
+
"criticality",
|
|
690
|
+
"queue_weight",
|
|
691
|
+
"creates_repo",
|
|
692
|
+
"auto_synced"
|
|
693
|
+
]) {
|
|
694
|
+
if (entry[key] !== undefined) {
|
|
695
|
+
preserved[key] = entry[key];
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return preserved;
|
|
699
|
+
}
|
|
700
|
+
function firstStringList(...candidates) {
|
|
701
|
+
for (const candidate of candidates) {
|
|
702
|
+
const list = readStringList(candidate);
|
|
703
|
+
if (list.length > 0) {
|
|
704
|
+
return list;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
function readStringList(candidate) {
|
|
710
|
+
if (!Array.isArray(candidate)) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
return candidate.filter((value) => typeof value === "string");
|
|
714
|
+
}
|
|
715
|
+
function isPlainRecord(candidate) {
|
|
716
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
|
|
720
|
+
var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
|
|
721
|
+
var FILE_TASK_PATTERN = /\.(task\.)?json$/;
|
|
722
|
+
function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
|
|
723
|
+
const configPath = options.configPath ?? resolve6(projectRoot, ".rig", "task-config.json");
|
|
724
|
+
const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
|
|
725
|
+
const spawnFn = options.spawn ?? spawnSync;
|
|
726
|
+
const ghBinary = options.ghBinary ?? "gh";
|
|
727
|
+
const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
|
|
728
|
+
return {
|
|
729
|
+
async listTasks() {
|
|
730
|
+
const rawConfig = readRawTaskConfig(configPath);
|
|
731
|
+
if (!rawConfig) {
|
|
732
|
+
const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
|
|
733
|
+
return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
|
|
734
|
+
}
|
|
735
|
+
const tasks = [];
|
|
736
|
+
const legacyTasks = await legacy.listTasks();
|
|
737
|
+
const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
|
|
738
|
+
for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
|
|
739
|
+
if (!isPlainRecord2(rawEntry)) {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const metadata = readMaterializedTaskMetadata(rawEntry);
|
|
743
|
+
if (metadata.taskSource?.kind === "github-issues") {
|
|
744
|
+
tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
|
|
748
|
+
const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
|
|
749
|
+
if (fileTask)
|
|
750
|
+
tasks.push(fileTask);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (!allowLocalFallback) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const legacyTask = legacyById.get(id);
|
|
757
|
+
if (legacyTask) {
|
|
758
|
+
tasks.push(legacyTask);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return tasks;
|
|
762
|
+
},
|
|
763
|
+
async getTask(id) {
|
|
764
|
+
const rawEntry = readRawTaskEntry(configPath, id);
|
|
765
|
+
if (!rawEntry) {
|
|
766
|
+
const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
|
|
767
|
+
return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
|
|
768
|
+
}
|
|
769
|
+
const metadata = readMaterializedTaskMetadata(rawEntry);
|
|
770
|
+
if (metadata.taskSource?.kind === "github-issues") {
|
|
771
|
+
return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
|
|
772
|
+
}
|
|
773
|
+
if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
|
|
774
|
+
return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
|
|
775
|
+
}
|
|
776
|
+
return allowLocalFallback ? legacy.getTask(id) : null;
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
function readMaterializedTaskMetadata(entry) {
|
|
781
|
+
const rawRig = entry._rig;
|
|
782
|
+
if (!isPlainRecord2(rawRig)) {
|
|
783
|
+
return {};
|
|
784
|
+
}
|
|
785
|
+
const rawSource = rawRig.taskSource;
|
|
786
|
+
const metadata = {};
|
|
787
|
+
if (isPlainRecord2(rawSource)) {
|
|
788
|
+
const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
|
|
789
|
+
if (kind.length > 0) {
|
|
790
|
+
metadata.taskSource = {
|
|
791
|
+
kind,
|
|
792
|
+
...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
|
|
793
|
+
...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
|
|
794
|
+
...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
|
|
795
|
+
...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
|
|
796
|
+
...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (typeof rawRig.sourceIssueId === "string") {
|
|
801
|
+
metadata.sourceIssueId = rawRig.sourceIssueId;
|
|
802
|
+
}
|
|
803
|
+
return metadata;
|
|
804
|
+
}
|
|
805
|
+
function readConfiguredFilesTaskSourcePath(projectRoot) {
|
|
806
|
+
const jsonPath = resolve6(projectRoot, "rig.config.json");
|
|
807
|
+
if (existsSync5(jsonPath)) {
|
|
808
|
+
try {
|
|
809
|
+
const parsed = JSON.parse(readFileSync4(jsonPath, "utf8"));
|
|
810
|
+
if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
|
|
811
|
+
const source = parsed.taskSource;
|
|
812
|
+
return source.kind === "files" && typeof source.path === "string" ? source.path : null;
|
|
813
|
+
}
|
|
814
|
+
} catch {
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const tsPath = resolve6(projectRoot, "rig.config.ts");
|
|
819
|
+
if (!existsSync5(tsPath)) {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const source = readFileSync4(tsPath, "utf8");
|
|
824
|
+
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
825
|
+
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
826
|
+
if (kind !== "files") {
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function readRawTaskEntry(configPath, taskId) {
|
|
835
|
+
const rawConfig = readRawTaskConfig(configPath);
|
|
836
|
+
if (!rawConfig) {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
|
|
840
|
+
return isPlainRecord2(entry) ? entry : null;
|
|
841
|
+
}
|
|
842
|
+
function readRawTaskConfig(configPath) {
|
|
843
|
+
if (!existsSync5(configPath)) {
|
|
844
|
+
return null;
|
|
845
|
+
}
|
|
846
|
+
const parsed = JSON.parse(readFileSync4(configPath, "utf8"));
|
|
847
|
+
return isPlainRecord2(parsed) ? parsed : null;
|
|
848
|
+
}
|
|
849
|
+
function stripLegacyTaskConfigMetadata2(raw) {
|
|
850
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
851
|
+
return tasks;
|
|
852
|
+
}
|
|
853
|
+
function listFileBackedTasks(projectRoot, sourcePath) {
|
|
854
|
+
const directory = resolve6(projectRoot, sourcePath);
|
|
855
|
+
if (!existsSync5(directory)) {
|
|
856
|
+
return [];
|
|
857
|
+
}
|
|
858
|
+
const tasks = [];
|
|
859
|
+
for (const name of readdirSync(directory)) {
|
|
860
|
+
if (!FILE_TASK_PATTERN.test(name))
|
|
861
|
+
continue;
|
|
862
|
+
const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
|
|
863
|
+
const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
|
|
864
|
+
if (task)
|
|
865
|
+
tasks.push(task);
|
|
866
|
+
}
|
|
867
|
+
return tasks;
|
|
868
|
+
}
|
|
869
|
+
function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
|
|
870
|
+
const file = findFileBackedTaskFile(resolve6(projectRoot, sourcePath), taskId);
|
|
871
|
+
if (!file) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
const raw = JSON.parse(readFileSync4(file, "utf8"));
|
|
875
|
+
if (!isPlainRecord2(raw)) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
id: typeof raw.id === "string" ? raw.id : taskId,
|
|
880
|
+
deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
|
|
881
|
+
status: typeof raw.status === "string" ? raw.status : "ready",
|
|
882
|
+
title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
|
|
883
|
+
...raw
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function findFileBackedTaskFile(directory, taskId) {
|
|
887
|
+
if (!existsSync5(directory)) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
for (const name of readdirSync(directory)) {
|
|
891
|
+
if (!FILE_TASK_PATTERN.test(name))
|
|
892
|
+
continue;
|
|
893
|
+
const file = join2(directory, name);
|
|
894
|
+
try {
|
|
895
|
+
if (!statSync(file).isFile())
|
|
896
|
+
continue;
|
|
897
|
+
const raw = JSON.parse(readFileSync4(file, "utf8"));
|
|
898
|
+
const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
|
|
899
|
+
const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
|
|
900
|
+
if (id === taskId) {
|
|
901
|
+
return file;
|
|
902
|
+
}
|
|
903
|
+
} catch {}
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
|
|
908
|
+
const source = requireGithubIssueSource(metadata, id);
|
|
909
|
+
const issue = runGh(bin, [
|
|
910
|
+
"issue",
|
|
911
|
+
"view",
|
|
912
|
+
String(id),
|
|
913
|
+
"--repo",
|
|
914
|
+
`${source.owner}/${source.repo}`,
|
|
915
|
+
"--json",
|
|
916
|
+
"number,title,body,labels,state,url,assignees"
|
|
917
|
+
], spawnFn);
|
|
918
|
+
return githubIssueToTask(issue, source, rawEntry);
|
|
919
|
+
}
|
|
920
|
+
function requireGithubIssueSource(metadata, id) {
|
|
921
|
+
const source = metadata.taskSource;
|
|
922
|
+
if (source?.kind === "github-issues" && source.owner && source.repo) {
|
|
923
|
+
return { owner: source.owner, repo: source.repo };
|
|
924
|
+
}
|
|
925
|
+
const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
926
|
+
if (parsed && parsed[3] === id) {
|
|
927
|
+
return { owner: parsed[1], repo: parsed[2] };
|
|
928
|
+
}
|
|
929
|
+
throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
|
|
930
|
+
}
|
|
931
|
+
function githubIssueToTask(issue, source, rawEntry) {
|
|
932
|
+
const labelNames = (issue.labels ?? []).map((label) => label.name);
|
|
933
|
+
const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
|
|
934
|
+
const roleLabel = labelNames.find((label) => label.startsWith("role:"));
|
|
935
|
+
const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
|
|
936
|
+
const body = issue.body ?? "";
|
|
937
|
+
const repo = `${source.owner}/${source.repo}`;
|
|
938
|
+
return {
|
|
939
|
+
id: String(issue.number),
|
|
940
|
+
deps: parseDeps(body),
|
|
941
|
+
status: githubStatusFor(issue),
|
|
942
|
+
title: issue.title,
|
|
943
|
+
body,
|
|
944
|
+
...scope.length > 0 ? { scope } : {},
|
|
945
|
+
...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
|
|
946
|
+
...validators.length > 0 ? { validators } : {},
|
|
947
|
+
...issue.url ? { url: issue.url } : {},
|
|
948
|
+
issueType: issueTypeFor(labelNames),
|
|
949
|
+
sourceIssueId: `${repo}#${issue.number}`,
|
|
950
|
+
parentChildDeps: parseParents(body),
|
|
951
|
+
labels: labelNames,
|
|
952
|
+
raw: issue,
|
|
953
|
+
source: "github-issues",
|
|
954
|
+
_rig: {
|
|
955
|
+
taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
|
|
956
|
+
sourceIssueId: `${repo}#${issue.number}`
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function githubStatusFor(issue) {
|
|
961
|
+
const state = (issue.state ?? "").toUpperCase();
|
|
962
|
+
if (state === "CLOSED")
|
|
963
|
+
return "closed";
|
|
964
|
+
const labelNames = (issue.labels ?? []).map((label) => label.name);
|
|
965
|
+
if (labelNames.includes("in-progress"))
|
|
966
|
+
return "in_progress";
|
|
967
|
+
if (labelNames.includes("blocked"))
|
|
968
|
+
return "blocked";
|
|
969
|
+
if (labelNames.includes("ready"))
|
|
970
|
+
return "ready";
|
|
971
|
+
if (labelNames.includes("under-review"))
|
|
972
|
+
return "under_review";
|
|
973
|
+
if (labelNames.includes("failed"))
|
|
974
|
+
return "failed";
|
|
975
|
+
if (labelNames.includes("cancelled"))
|
|
976
|
+
return "cancelled";
|
|
977
|
+
return "open";
|
|
978
|
+
}
|
|
979
|
+
function selectedGitHubEnv() {
|
|
980
|
+
const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() ?? "";
|
|
981
|
+
return { GH_TOKEN: token, GITHUB_TOKEN: token };
|
|
982
|
+
}
|
|
983
|
+
function ghSpawnOptions() {
|
|
984
|
+
return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
|
|
985
|
+
}
|
|
986
|
+
function runGh(bin, args, spawnFn) {
|
|
987
|
+
const res = spawnFn(bin, [...args], ghSpawnOptions());
|
|
988
|
+
assertGhSuccess(args, res);
|
|
989
|
+
if (!res.stdout || res.stdout.trim() === "") {
|
|
990
|
+
throw new Error(`gh ${args.join(" ")} returned empty stdout`);
|
|
991
|
+
}
|
|
992
|
+
return JSON.parse(res.stdout);
|
|
993
|
+
}
|
|
994
|
+
function assertGhSuccess(args, res) {
|
|
995
|
+
if (res.error) {
|
|
996
|
+
const msg = res.error.message ?? String(res.error);
|
|
997
|
+
throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
|
|
998
|
+
}
|
|
999
|
+
if (res.status !== 0) {
|
|
1000
|
+
throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function parseDeps(body) {
|
|
1004
|
+
return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
|
|
1005
|
+
}
|
|
1006
|
+
function parseParents(body) {
|
|
1007
|
+
return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
|
|
1008
|
+
}
|
|
1009
|
+
function parseIssueRefs(body, pattern) {
|
|
1010
|
+
const match = body.match(pattern);
|
|
1011
|
+
if (!match)
|
|
1012
|
+
return [];
|
|
1013
|
+
return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
|
|
1014
|
+
}
|
|
1015
|
+
function issueTypeFor(labels) {
|
|
1016
|
+
const typed = labels.find((label) => label.startsWith("type:"));
|
|
1017
|
+
if (typed)
|
|
1018
|
+
return typed.slice("type:".length);
|
|
1019
|
+
if (labels.includes("epic"))
|
|
1020
|
+
return "epic";
|
|
1021
|
+
return "task";
|
|
1022
|
+
}
|
|
1023
|
+
function isPlainRecord2(candidate) {
|
|
1024
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// packages/runtime/src/control-plane/tasks/source-lifecycle.ts
|
|
1028
|
+
function hasRunnableTaskSource(source) {
|
|
1029
|
+
return Boolean(source && typeof source === "object" && !Array.isArray(source));
|
|
1030
|
+
}
|
|
1031
|
+
async function getPluginTask(projectRoot, taskId) {
|
|
1032
|
+
const ctx = await buildPluginHostContext(projectRoot);
|
|
1033
|
+
const [source] = ctx?.taskSourceRegistry.list() ?? [];
|
|
1034
|
+
if (!hasRunnableTaskSource(source)) {
|
|
1035
|
+
return ctx ? { configured: false, sourceKind: null, task: null } : null;
|
|
1036
|
+
}
|
|
1037
|
+
const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
|
|
1038
|
+
return {
|
|
1039
|
+
configured: true,
|
|
1040
|
+
sourceKind: source.kind,
|
|
1041
|
+
task
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
async function readConfiguredTaskSourceTask(projectRoot, taskId) {
|
|
1045
|
+
const pluginResult = await getPluginTask(projectRoot, taskId);
|
|
1046
|
+
if (pluginResult)
|
|
1047
|
+
return pluginResult;
|
|
1048
|
+
const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
|
|
1049
|
+
return {
|
|
1050
|
+
configured: false,
|
|
1051
|
+
sourceKind: null,
|
|
1052
|
+
task
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// packages/runtime/src/control-plane/native/task-state.ts
|
|
1057
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
1058
|
+
import { basename as basename5, resolve as resolve11 } from "path";
|
|
1059
|
+
|
|
1060
|
+
// packages/runtime/src/control-plane/state-sync/types.ts
|
|
1061
|
+
var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
|
|
1062
|
+
"draft",
|
|
1063
|
+
"open",
|
|
1064
|
+
"ready",
|
|
1065
|
+
"queued",
|
|
1066
|
+
"in_progress",
|
|
1067
|
+
"under_review",
|
|
1068
|
+
"blocked",
|
|
1069
|
+
"completed",
|
|
1070
|
+
"cancelled"
|
|
1071
|
+
]);
|
|
1072
|
+
// packages/runtime/src/control-plane/native/git-native.ts
|
|
1073
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
1074
|
+
import { dirname as dirname5, isAbsolute, resolve as resolve7 } from "path";
|
|
1075
|
+
var sharedGitNativeOutputDir = resolve7(tmpdir3(), "rig-native");
|
|
1076
|
+
var sharedGitNativeOutputPath = resolve7(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
|
|
1077
|
+
|
|
1078
|
+
// packages/runtime/src/control-plane/native/utils.ts
|
|
1079
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
|
|
1080
|
+
import { resolve as resolve10 } from "path";
|
|
1081
|
+
|
|
1082
|
+
// packages/runtime/src/layout.ts
|
|
1083
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1084
|
+
import { basename as basename4, dirname as dirname6, resolve as resolve8 } from "path";
|
|
1085
|
+
var RIG_DEFINITION_DIRNAME = "rig";
|
|
1086
|
+
var RIG_ARTIFACTS_DIRNAME = "artifacts";
|
|
1087
|
+
function resolveMonorepoRoot(projectRoot) {
|
|
1088
|
+
const normalizedProjectRoot = resolve8(projectRoot);
|
|
1089
|
+
const explicit = process.env.MONOREPO_ROOT?.trim();
|
|
1090
|
+
if (explicit) {
|
|
1091
|
+
const explicitRoot = resolve8(explicit);
|
|
1092
|
+
const explicitParent = dirname6(explicitRoot);
|
|
1093
|
+
if (basename4(explicitParent) === ".worktrees") {
|
|
1094
|
+
const owner = dirname6(explicitParent);
|
|
1095
|
+
const ownerHasGit = existsSync6(resolve8(owner, ".git"));
|
|
1096
|
+
const ownerHasTaskConfig = existsSync6(resolve8(owner, ".rig", "task-config.json"));
|
|
1097
|
+
const ownerHasRigConfig = existsSync6(resolve8(owner, "rig.config.ts"));
|
|
1098
|
+
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1099
|
+
return owner;
|
|
1100
|
+
}
|
|
1101
|
+
throw new Error(`MONOREPO_ROOT points to worktree ${explicitRoot}, but the owner checkout is incomplete at ${owner}.`);
|
|
1102
|
+
}
|
|
1103
|
+
if (!existsSync6(resolve8(explicitRoot, ".git"))) {
|
|
1104
|
+
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but no git checkout was found there.`);
|
|
1105
|
+
}
|
|
1106
|
+
const hasTaskConfig = existsSync6(resolve8(explicitRoot, ".rig", "task-config.json"));
|
|
1107
|
+
const hasRigConfig = existsSync6(resolve8(explicitRoot, "rig.config.ts"));
|
|
1108
|
+
if (!hasTaskConfig && !hasRigConfig) {
|
|
1109
|
+
throw new Error(`MONOREPO_ROOT points to ${explicitRoot}, but neither .rig/task-config.json nor rig.config.ts exists there.`);
|
|
1110
|
+
}
|
|
1111
|
+
return explicitRoot;
|
|
1112
|
+
}
|
|
1113
|
+
const projectParent = dirname6(normalizedProjectRoot);
|
|
1114
|
+
if (basename4(projectParent) === ".worktrees") {
|
|
1115
|
+
const worktreeOwner = dirname6(projectParent);
|
|
1116
|
+
const ownerHasGit = existsSync6(resolve8(worktreeOwner, ".git"));
|
|
1117
|
+
const ownerHasTaskConfig = existsSync6(resolve8(worktreeOwner, ".rig", "task-config.json"));
|
|
1118
|
+
const ownerHasRigConfig = existsSync6(resolve8(worktreeOwner, "rig.config.ts"));
|
|
1119
|
+
if (ownerHasGit && (ownerHasTaskConfig || ownerHasRigConfig)) {
|
|
1120
|
+
return worktreeOwner;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return normalizedProjectRoot;
|
|
1124
|
+
}
|
|
1125
|
+
function resolveRuntimeWorkspaceLayout(workspaceDir) {
|
|
1126
|
+
const root = resolve8(workspaceDir);
|
|
1127
|
+
const rigRoot = resolve8(root, ".rig");
|
|
1128
|
+
const logsDir = resolve8(rigRoot, "logs");
|
|
1129
|
+
const stateDir = resolve8(rigRoot, "state");
|
|
1130
|
+
const runtimeDir = resolve8(rigRoot, "runtime");
|
|
1131
|
+
const binDir = resolve8(rigRoot, "bin");
|
|
1132
|
+
return {
|
|
1133
|
+
workspaceDir: root,
|
|
1134
|
+
rigRoot,
|
|
1135
|
+
stateDir,
|
|
1136
|
+
logsDir,
|
|
1137
|
+
artifactsRoot: resolve8(root, RIG_ARTIFACTS_DIRNAME),
|
|
1138
|
+
runtimeDir,
|
|
1139
|
+
homeDir: resolve8(rigRoot, "home"),
|
|
1140
|
+
tmpDir: resolve8(rigRoot, "tmp"),
|
|
1141
|
+
cacheDir: resolve8(rigRoot, "cache"),
|
|
1142
|
+
sessionDir: resolve8(rigRoot, "session"),
|
|
1143
|
+
binDir,
|
|
1144
|
+
distDir: resolve8(rigRoot, "dist"),
|
|
1145
|
+
pluginBinDir: resolve8(binDir, "plugins"),
|
|
1146
|
+
contextPath: resolve8(rigRoot, "runtime-context.json"),
|
|
1147
|
+
controlPlaneEventsFile: resolve8(logsDir, "control-plane.events.jsonl")
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function resolveActiveRuntimeWorkspaceRoot(monorepoRoot) {
|
|
1151
|
+
const explicit = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1152
|
+
if (!explicit) {
|
|
1153
|
+
throw new Error("No active runtime workspace. Set RIG_TASK_WORKSPACE or provision a task runtime first.");
|
|
1154
|
+
}
|
|
1155
|
+
return resolve8(explicit);
|
|
1156
|
+
}
|
|
1157
|
+
function resolveRigLayout(projectRoot) {
|
|
1158
|
+
const monorepoRoot = resolveMonorepoRoot(projectRoot);
|
|
1159
|
+
const definitionRoot = resolve8(projectRoot, RIG_DEFINITION_DIRNAME);
|
|
1160
|
+
const runtimeWorkspaceRoot = resolveActiveRuntimeWorkspaceRoot(monorepoRoot);
|
|
1161
|
+
const runtimeLayout = resolveRuntimeWorkspaceLayout(runtimeWorkspaceRoot);
|
|
1162
|
+
const policyDir = resolve8(definitionRoot, "policy");
|
|
1163
|
+
return {
|
|
1164
|
+
projectRoot,
|
|
1165
|
+
monorepoRoot,
|
|
1166
|
+
definitionRoot,
|
|
1167
|
+
runtimeWorkspaceRoot,
|
|
1168
|
+
stateRoot: runtimeLayout.rigRoot,
|
|
1169
|
+
artifactsRoot: runtimeLayout.artifactsRoot,
|
|
1170
|
+
configPath: resolve8(definitionRoot, "config.sh"),
|
|
1171
|
+
taskConfigPath: resolve8(runtimeWorkspaceRoot, ".rig", "task-config.json"),
|
|
1172
|
+
policyDir,
|
|
1173
|
+
policyFile: resolve8(policyDir, "policy.json"),
|
|
1174
|
+
pluginsDir: resolve8(definitionRoot, "plugins"),
|
|
1175
|
+
hooksDir: resolve8(definitionRoot, "hooks"),
|
|
1176
|
+
toolsDir: resolve8(definitionRoot, "tools"),
|
|
1177
|
+
templatesDir: resolve8(definitionRoot, "templates"),
|
|
1178
|
+
validationDir: resolve8(definitionRoot, "validation"),
|
|
1179
|
+
stateDir: runtimeLayout.stateDir,
|
|
1180
|
+
logsDir: runtimeLayout.logsDir,
|
|
1181
|
+
notificationsDir: resolve8(definitionRoot, "notifications"),
|
|
1182
|
+
runtimeDir: runtimeLayout.runtimeDir,
|
|
1183
|
+
distDir: runtimeLayout.distDir,
|
|
1184
|
+
binDir: runtimeLayout.binDir,
|
|
1185
|
+
pluginBinDir: runtimeLayout.pluginBinDir,
|
|
1186
|
+
keybindingsPath: resolve8(definitionRoot, "keybindings.json"),
|
|
1187
|
+
controlPlaneEventsFile: runtimeLayout.controlPlaneEventsFile
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// packages/runtime/src/control-plane/native/runtime-native.ts
|
|
1192
|
+
import { dlopen, ptr, suffix, toBuffer } from "bun:ffi";
|
|
1193
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, renameSync, rmSync, statSync as statSync2 } from "fs";
|
|
1194
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
1195
|
+
import { dirname as dirname7, resolve as resolve9 } from "path";
|
|
1196
|
+
var sharedNativeRuntimeOutputDir = resolve9(tmpdir4(), "rig-native");
|
|
1197
|
+
var sharedNativeRuntimeOutputPath = resolve9(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
|
|
1198
|
+
var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
|
|
1199
|
+
var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
|
|
1200
|
+
async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOutputPath, options = {}) {
|
|
1201
|
+
if (await buildNativeRuntimeLibrary(outputPath, options)) {
|
|
1202
|
+
return outputPath;
|
|
1203
|
+
}
|
|
1204
|
+
return !options.force && existsSync7(outputPath) ? outputPath : null;
|
|
1205
|
+
}
|
|
1206
|
+
async function loadNativeRuntimeLibrary() {
|
|
1207
|
+
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
for (const candidate of nativeRuntimeLibraryCandidates()) {
|
|
1211
|
+
if (!candidate || !existsSync7(candidate)) {
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
const loaded = tryDlopenNativeRuntimeLibrary(candidate);
|
|
1215
|
+
if (loaded) {
|
|
1216
|
+
return loaded;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const builtLibraryPath = await ensureNativeRuntimeLibraryPath(sharedNativeRuntimeOutputPath, { force: true });
|
|
1220
|
+
if (!builtLibraryPath) {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
return tryDlopenNativeRuntimeLibrary(builtLibraryPath);
|
|
1224
|
+
}
|
|
1225
|
+
function nativePackageLibraryCandidates(fromDir, names) {
|
|
1226
|
+
const candidates = [];
|
|
1227
|
+
let cursor = resolve9(fromDir);
|
|
1228
|
+
for (let index = 0;index < 8; index += 1) {
|
|
1229
|
+
for (const name of names) {
|
|
1230
|
+
candidates.push(resolve9(cursor, "native", `${process.platform}-${process.arch}`, name), resolve9(cursor, "native", `${process.platform}-${process.arch}`, "lib", name), resolve9(cursor, "native", name), resolve9(cursor, "native", "lib", name));
|
|
1231
|
+
}
|
|
1232
|
+
const parent = dirname7(cursor);
|
|
1233
|
+
if (parent === cursor)
|
|
1234
|
+
break;
|
|
1235
|
+
cursor = parent;
|
|
1236
|
+
}
|
|
1237
|
+
return candidates;
|
|
1238
|
+
}
|
|
1239
|
+
function nativeRuntimeLibraryCandidates() {
|
|
1240
|
+
const explicit = process.env.RIG_NATIVE_RUNTIME_LIB?.trim() || "";
|
|
1241
|
+
const execDir = process.execPath?.trim() ? dirname7(process.execPath.trim()) : "";
|
|
1242
|
+
const platformSpecific = `runtime-native-${process.platform}-${process.arch}.${suffix}`;
|
|
1243
|
+
return [...new Set([
|
|
1244
|
+
explicit,
|
|
1245
|
+
...nativePackageLibraryCandidates(import.meta.dir, [colocatedNativeRuntimeFileName, platformSpecific]),
|
|
1246
|
+
execDir ? resolve9(execDir, colocatedNativeRuntimeFileName) : "",
|
|
1247
|
+
execDir ? resolve9(execDir, platformSpecific) : "",
|
|
1248
|
+
execDir ? resolve9(execDir, "..", colocatedNativeRuntimeFileName) : "",
|
|
1249
|
+
execDir ? resolve9(execDir, "..", platformSpecific) : "",
|
|
1250
|
+
execDir ? resolve9(execDir, "lib", colocatedNativeRuntimeFileName) : "",
|
|
1251
|
+
execDir ? resolve9(execDir, "..", "lib", colocatedNativeRuntimeFileName) : "",
|
|
1252
|
+
sharedNativeRuntimeOutputPath
|
|
1253
|
+
].filter(Boolean))];
|
|
1254
|
+
}
|
|
1255
|
+
function resolveNativeRuntimeSourcePath() {
|
|
1256
|
+
const explicit = process.env.RIG_NATIVE_RUNTIME_SOURCE?.trim();
|
|
1257
|
+
if (explicit && existsSync7(explicit)) {
|
|
1258
|
+
return explicit;
|
|
1259
|
+
}
|
|
1260
|
+
const bundled = resolve9(import.meta.dir, "../../../native/snapshot.zig");
|
|
1261
|
+
return existsSync7(bundled) ? bundled : null;
|
|
1262
|
+
}
|
|
1263
|
+
async function buildNativeRuntimeLibrary(outputPath, options = {}) {
|
|
1264
|
+
if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
const zigBinary = Bun.which("zig");
|
|
1268
|
+
const sourcePath = resolveNativeRuntimeSourcePath();
|
|
1269
|
+
if (!zigBinary || !sourcePath) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
const tempOutputPath = `${outputPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
1273
|
+
try {
|
|
1274
|
+
mkdirSync3(dirname7(outputPath), { recursive: true });
|
|
1275
|
+
const needsBuild = options.force === true || !existsSync7(outputPath) || statSync2(sourcePath).mtimeMs > statSync2(outputPath).mtimeMs;
|
|
1276
|
+
if (!needsBuild) {
|
|
1277
|
+
return true;
|
|
1278
|
+
}
|
|
1279
|
+
const build = Bun.spawn([
|
|
1280
|
+
zigBinary,
|
|
1281
|
+
"build-lib",
|
|
1282
|
+
sourcePath,
|
|
1283
|
+
"-dynamic",
|
|
1284
|
+
"-O",
|
|
1285
|
+
"ReleaseFast",
|
|
1286
|
+
`-femit-bin=${tempOutputPath}`
|
|
1287
|
+
], {
|
|
1288
|
+
cwd: import.meta.dir,
|
|
1289
|
+
stdout: "pipe",
|
|
1290
|
+
stderr: "pipe"
|
|
1291
|
+
});
|
|
1292
|
+
const exitCode = await build.exited;
|
|
1293
|
+
if (exitCode !== 0 || !existsSync7(tempOutputPath)) {
|
|
1294
|
+
rmSync(tempOutputPath, { force: true });
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
renameSync(tempOutputPath, outputPath);
|
|
1298
|
+
return true;
|
|
1299
|
+
} catch {
|
|
1300
|
+
rmSync(tempOutputPath, { force: true });
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function tryDlopenNativeRuntimeLibrary(outputPath) {
|
|
1305
|
+
try {
|
|
1306
|
+
return dlopen(outputPath, {
|
|
1307
|
+
rig_scope_match: {
|
|
1308
|
+
args: ["ptr", "ptr"],
|
|
1309
|
+
returns: "u8"
|
|
1310
|
+
},
|
|
1311
|
+
snapshot_capture: {
|
|
1312
|
+
args: ["ptr", "u64", "ptr", "u64"],
|
|
1313
|
+
returns: "ptr"
|
|
1314
|
+
},
|
|
1315
|
+
snapshot_delta: {
|
|
1316
|
+
args: ["ptr", "ptr"],
|
|
1317
|
+
returns: "ptr"
|
|
1318
|
+
},
|
|
1319
|
+
snapshot_store_delta: {
|
|
1320
|
+
args: ["ptr", "ptr", "ptr", "u64", "ptr", "u64", "ptr", "u64", "ptr", "u64"],
|
|
1321
|
+
returns: "ptr"
|
|
1322
|
+
},
|
|
1323
|
+
snapshot_inspect_delta: {
|
|
1324
|
+
args: ["ptr", "u64"],
|
|
1325
|
+
returns: "ptr"
|
|
1326
|
+
},
|
|
1327
|
+
snapshot_apply_delta: {
|
|
1328
|
+
args: ["ptr", "u64", "ptr", "u64"],
|
|
1329
|
+
returns: "ptr"
|
|
1330
|
+
},
|
|
1331
|
+
snapshot_release: {
|
|
1332
|
+
args: ["ptr"],
|
|
1333
|
+
returns: "void"
|
|
1334
|
+
},
|
|
1335
|
+
runtime_hash_file: {
|
|
1336
|
+
args: ["ptr", "u64"],
|
|
1337
|
+
returns: "ptr"
|
|
1338
|
+
},
|
|
1339
|
+
runtime_hash_tree: {
|
|
1340
|
+
args: ["ptr", "u64"],
|
|
1341
|
+
returns: "ptr"
|
|
1342
|
+
},
|
|
1343
|
+
runtime_prepare_paths: {
|
|
1344
|
+
args: ["ptr", "u64", "ptr", "u64", "ptr", "u64", "ptr", "u64", "ptr", "u64"],
|
|
1345
|
+
returns: "ptr"
|
|
1346
|
+
},
|
|
1347
|
+
runtime_link_dependency_layer: {
|
|
1348
|
+
args: ["ptr", "u64", "ptr", "u64"],
|
|
1349
|
+
returns: "ptr"
|
|
1350
|
+
},
|
|
1351
|
+
runtime_scan_worktrees: {
|
|
1352
|
+
args: ["ptr", "u64"],
|
|
1353
|
+
returns: "ptr"
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
} catch {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// packages/runtime/src/control-plane/native/utils.ts
|
|
1362
|
+
function resolveMonorepoRoot2(projectRoot) {
|
|
1363
|
+
return resolveMonorepoRoot(projectRoot);
|
|
1364
|
+
}
|
|
1365
|
+
var scopeRegexCache = new Map;
|
|
1366
|
+
function runCapture(command, cwd, env) {
|
|
1367
|
+
const result = Bun.spawnSync(command, {
|
|
1368
|
+
cwd,
|
|
1369
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
1370
|
+
stdout: "pipe",
|
|
1371
|
+
stderr: "pipe"
|
|
1372
|
+
});
|
|
1373
|
+
return {
|
|
1374
|
+
exitCode: result.exitCode,
|
|
1375
|
+
stdout: result.stdout.toString(),
|
|
1376
|
+
stderr: result.stderr.toString()
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function readJsonFile(path, fallback) {
|
|
1380
|
+
if (!existsSync8(path)) {
|
|
1381
|
+
return fallback;
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
1385
|
+
} catch {
|
|
1386
|
+
return fallback;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function nowIso() {
|
|
1390
|
+
return new Date().toISOString();
|
|
1391
|
+
}
|
|
1392
|
+
function resolveHarnessPaths(projectRoot) {
|
|
1393
|
+
const hasRuntimeWorkspace = Boolean(process.env.RIG_TASK_WORKSPACE?.trim());
|
|
1394
|
+
const monorepoRoot = resolveMonorepoRoot2(projectRoot);
|
|
1395
|
+
const harnessRoot = resolve10(projectRoot, "rig");
|
|
1396
|
+
const stateRoot = resolve10(projectRoot, ".rig");
|
|
1397
|
+
const layout = hasRuntimeWorkspace ? resolveRigLayout(projectRoot) : null;
|
|
1398
|
+
const stateDir = layout?.stateDir ?? resolve10(stateRoot, "state");
|
|
1399
|
+
const logsDir = layout?.logsDir ?? resolve10(stateRoot, "logs");
|
|
1400
|
+
const artifactsDir = layout?.artifactsRoot ?? resolve10(monorepoRoot, "artifacts");
|
|
1401
|
+
const taskConfigPath = layout?.taskConfigPath ?? resolve10(monorepoRoot, ".rig", "task-config.json");
|
|
1402
|
+
const binDir = layout?.binDir ?? resolve10(stateRoot, "bin");
|
|
1403
|
+
return {
|
|
1404
|
+
harnessRoot,
|
|
1405
|
+
stateDir: process.env.RIG_STATE_DIR || stateDir,
|
|
1406
|
+
artifactsDir,
|
|
1407
|
+
logsDir: process.env.RIG_LOGS_DIR || logsDir,
|
|
1408
|
+
binDir,
|
|
1409
|
+
hooksDir: resolve10(harnessRoot, "hooks"),
|
|
1410
|
+
validationDir: resolve10(harnessRoot, "validation"),
|
|
1411
|
+
taskConfigPath,
|
|
1412
|
+
sessionPath: process.env.RIG_SESSION_FILE || resolve10(stateRoot, "session", "session.json"),
|
|
1413
|
+
monorepoRoot,
|
|
1414
|
+
tsApiTestsDir: process.env.TS_API_TESTS_DIR || resolve10(monorepoRoot, "TSAPITests"),
|
|
1415
|
+
taskRepoCommitsPath: resolve10(stateDir, "task-repo-commits.json"),
|
|
1416
|
+
baseRepoPinsPath: resolve10(stateDir, "base-repo-pins.json"),
|
|
1417
|
+
failedApproachesPath: resolve10(stateDir, "failed_approaches.md"),
|
|
1418
|
+
agentProfilePath: resolve10(stateDir, "agent-profile.json"),
|
|
1419
|
+
reviewProfilePath: resolve10(stateDir, "review-profile.json")
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
// packages/runtime/src/control-plane/state-sync/reconcile.ts
|
|
1423
|
+
var STALE_CLAIM_MS = 24 * 60 * 60 * 1000;
|
|
1424
|
+
// packages/runtime/src/control-plane/native/task-state.ts
|
|
1425
|
+
function readTaskConfig(projectRoot) {
|
|
1426
|
+
const raw = readJsonFile(resolveTaskConfigPath(projectRoot), {});
|
|
1427
|
+
return stripTaskConfigMetadata(raw);
|
|
1428
|
+
}
|
|
1429
|
+
function readSourceTaskConfig(projectRoot) {
|
|
1430
|
+
const raw = readAndSyncSourceTaskConfig(projectRoot);
|
|
1431
|
+
return stripTaskConfigMetadata(raw);
|
|
1432
|
+
}
|
|
1433
|
+
function stripTaskConfigMetadata(raw) {
|
|
1434
|
+
const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
|
|
1435
|
+
return tasks;
|
|
1436
|
+
}
|
|
1437
|
+
function coerceValidationDescriptions(candidate) {
|
|
1438
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
1439
|
+
return {};
|
|
1440
|
+
}
|
|
1441
|
+
const descriptions = {};
|
|
1442
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
1443
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1444
|
+
descriptions[key] = value;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
return descriptions;
|
|
1448
|
+
}
|
|
1449
|
+
function readValidationDescriptionsFromMeta(meta) {
|
|
1450
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
return meta.validation_descriptions;
|
|
1454
|
+
}
|
|
1455
|
+
function lookupTask(projectRoot, input) {
|
|
1456
|
+
if (!input) {
|
|
1457
|
+
return "";
|
|
1458
|
+
}
|
|
1459
|
+
if (!input.startsWith("bd-")) {
|
|
1460
|
+
return input;
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
const taskConfig2 = readTaskConfig(projectRoot);
|
|
1464
|
+
if (taskConfig2[input]) {
|
|
1465
|
+
return input;
|
|
1466
|
+
}
|
|
1467
|
+
} catch {}
|
|
1468
|
+
const taskConfig = readSourceTaskConfig(projectRoot);
|
|
1469
|
+
return taskConfig[input] ? input : "";
|
|
1470
|
+
}
|
|
1471
|
+
function artifactDirForId(projectRoot, id) {
|
|
1472
|
+
const workspaceDir = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1473
|
+
if (workspaceDir) {
|
|
1474
|
+
const worktreeArtifacts = resolve11(workspaceDir, "artifacts", id);
|
|
1475
|
+
if (existsSync9(worktreeArtifacts) || existsSync9(resolve11(workspaceDir, "artifacts"))) {
|
|
1476
|
+
return worktreeArtifacts;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1481
|
+
return resolve11(paths.artifactsDir, id);
|
|
1482
|
+
} catch {
|
|
1483
|
+
return resolve11(resolveMonorepoRoot2(projectRoot), "artifacts", id);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function resolveTaskConfigPath(projectRoot) {
|
|
1487
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
1488
|
+
if (existsSync9(paths.taskConfigPath)) {
|
|
1489
|
+
return paths.taskConfigPath;
|
|
1490
|
+
}
|
|
1491
|
+
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1492
|
+
if (existsSync9(candidate)) {
|
|
1493
|
+
return candidate;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
throw new Error(`Task config missing at ${paths.taskConfigPath}.`);
|
|
1497
|
+
}
|
|
1498
|
+
function findSourceTaskConfigPath(projectRoot) {
|
|
1499
|
+
for (const candidate of sourceTaskConfigCandidates(projectRoot)) {
|
|
1500
|
+
if (existsSync9(candidate)) {
|
|
1501
|
+
return candidate;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
var FILE_TASK_PATTERN2 = /\.(task\.)?json$/;
|
|
1507
|
+
function readAndSyncSourceTaskConfig(projectRoot) {
|
|
1508
|
+
const sourcePath = findSourceTaskConfigPath(projectRoot);
|
|
1509
|
+
const raw = sourcePath ? readJsonFile(sourcePath, {}) : readConfiguredFileTaskConfig(projectRoot);
|
|
1510
|
+
const synced = synchronizeTaskConfigWithTracker(projectRoot, raw);
|
|
1511
|
+
if (sourcePath && synced.updated) {
|
|
1512
|
+
try {
|
|
1513
|
+
writeFileSync4(sourcePath, `${JSON.stringify(synced.config, null, 2)}
|
|
1514
|
+
`, "utf-8");
|
|
1515
|
+
} catch {}
|
|
1516
|
+
}
|
|
1517
|
+
return synced.config;
|
|
1518
|
+
}
|
|
1519
|
+
function synchronizeTaskConfigWithTracker(projectRoot, rawConfig) {
|
|
1520
|
+
const issues = readSourceIssueRecords(projectRoot);
|
|
1521
|
+
if (issues.length === 0) {
|
|
1522
|
+
return { config: rawConfig, updated: false };
|
|
1523
|
+
}
|
|
1524
|
+
const taskConfig = stripTaskConfigMetadata(rawConfig);
|
|
1525
|
+
const mergedConfig = { ...taskConfig };
|
|
1526
|
+
const validationDescriptions = coerceValidationDescriptions(rawConfig.validation_descriptions);
|
|
1527
|
+
const metaValidationDescriptions = coerceValidationDescriptions(readValidationDescriptionsFromMeta(rawConfig._meta));
|
|
1528
|
+
let updated = false;
|
|
1529
|
+
for (const issue of issues) {
|
|
1530
|
+
if (issue.issueType !== "task") {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (mergedConfig[issue.id] && !shouldRefreshAutoSyncedTaskConfigEntry(mergedConfig[issue.id])) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
mergedConfig[issue.id] = buildAutoSyncedTaskConfigEntry(issue);
|
|
1537
|
+
updated = true;
|
|
1538
|
+
}
|
|
1539
|
+
return {
|
|
1540
|
+
config: {
|
|
1541
|
+
...mergedConfig,
|
|
1542
|
+
...Object.keys(validationDescriptions).length > 0 ? { validation_descriptions: validationDescriptions } : {},
|
|
1543
|
+
...Object.keys(metaValidationDescriptions).length > 0 ? { _meta: { validation_descriptions: metaValidationDescriptions } } : {}
|
|
1544
|
+
},
|
|
1545
|
+
updated
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function shouldRefreshAutoSyncedTaskConfigEntry(entry) {
|
|
1549
|
+
if (!entry || typeof entry !== "object") {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
const candidate = entry;
|
|
1553
|
+
if (!candidate.auto_synced) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
if (!Array.isArray(candidate.scope) || candidate.scope.length === 0) {
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1559
|
+
if (candidate.scope.some((glob) => typeof glob !== "string" || glob.trim().length === 0)) {
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
return !candidate.role;
|
|
1563
|
+
}
|
|
1564
|
+
function readSourceIssueRecords(projectRoot) {
|
|
1565
|
+
const issuesPath = resolve11(resolveMonorepoRoot2(projectRoot), ".beads", "issues.jsonl");
|
|
1566
|
+
if (!existsSync9(issuesPath)) {
|
|
1567
|
+
return [];
|
|
1568
|
+
}
|
|
1569
|
+
const records = [];
|
|
1570
|
+
for (const line of readFileSync6(issuesPath, "utf-8").split(/\r?\n/)) {
|
|
1571
|
+
const trimmed = line.trim();
|
|
1572
|
+
if (!trimmed) {
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
const parsed = JSON.parse(trimmed);
|
|
1577
|
+
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
1578
|
+
if (!id) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
records.push({
|
|
1582
|
+
id,
|
|
1583
|
+
title: typeof parsed.title === "string" ? parsed.title.trim() : "",
|
|
1584
|
+
issueType: typeof parsed.issue_type === "string" ? parsed.issue_type.trim() : null,
|
|
1585
|
+
labels: Array.isArray(parsed.labels) ? parsed.labels.filter((label) => typeof label === "string") : []
|
|
1586
|
+
});
|
|
1587
|
+
} catch {}
|
|
1588
|
+
}
|
|
1589
|
+
return records;
|
|
1590
|
+
}
|
|
1591
|
+
function buildAutoSyncedTaskConfigEntry(issue) {
|
|
1592
|
+
return {
|
|
1593
|
+
auto_synced: true,
|
|
1594
|
+
role: inferAutoSyncedTaskRole(issue),
|
|
1595
|
+
scope: inferAutoSyncedTaskScope(issue),
|
|
1596
|
+
validation: []
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
function inferAutoSyncedTaskRole(issue) {
|
|
1600
|
+
for (const label of issue.labels) {
|
|
1601
|
+
if (label === "role:architect")
|
|
1602
|
+
return "architect";
|
|
1603
|
+
if (label === "role:extractor")
|
|
1604
|
+
return "extractor";
|
|
1605
|
+
if (label === "role:mechanic")
|
|
1606
|
+
return "mechanic";
|
|
1607
|
+
if (label === "role:verifier")
|
|
1608
|
+
return "verifier";
|
|
1609
|
+
}
|
|
1610
|
+
if (/\bDESIGN\b/i.test(issue.title)) {
|
|
1611
|
+
return "architect";
|
|
1612
|
+
}
|
|
1613
|
+
if (/\bInitialize\b/i.test(issue.title)) {
|
|
1614
|
+
return "mechanic";
|
|
1615
|
+
}
|
|
1616
|
+
return "extractor";
|
|
1617
|
+
}
|
|
1618
|
+
function inferAutoSyncedTaskScope(issue) {
|
|
1619
|
+
return [`artifacts/${issue.id}/**`];
|
|
1620
|
+
}
|
|
1621
|
+
function readConfiguredFileTaskConfig(projectRoot) {
|
|
1622
|
+
const sourcePath = readConfiguredFilesTaskSourcePath2(projectRoot);
|
|
1623
|
+
if (!sourcePath) {
|
|
1624
|
+
return {};
|
|
1625
|
+
}
|
|
1626
|
+
const directory = resolve11(projectRoot, sourcePath);
|
|
1627
|
+
if (!existsSync9(directory)) {
|
|
1628
|
+
return {};
|
|
1629
|
+
}
|
|
1630
|
+
const config = {};
|
|
1631
|
+
for (const name of readdirSync2(directory)) {
|
|
1632
|
+
if (!FILE_TASK_PATTERN2.test(name))
|
|
1633
|
+
continue;
|
|
1634
|
+
const file = resolve11(directory, name);
|
|
1635
|
+
try {
|
|
1636
|
+
if (!statSync3(file).isFile())
|
|
1637
|
+
continue;
|
|
1638
|
+
const raw = JSON.parse(readFileSync6(file, "utf8"));
|
|
1639
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1640
|
+
continue;
|
|
1641
|
+
const record = raw;
|
|
1642
|
+
const inferredId = basename5(name).replace(FILE_TASK_PATTERN2, "");
|
|
1643
|
+
const id = typeof record.id === "string" && record.id.trim().length > 0 ? record.id.trim() : inferredId;
|
|
1644
|
+
config[id] = fileTaskToConfigEntry(record, { kind: "files", path: sourcePath });
|
|
1645
|
+
} catch {}
|
|
1646
|
+
}
|
|
1647
|
+
return config;
|
|
1648
|
+
}
|
|
1649
|
+
function fileTaskToConfigEntry(task, source) {
|
|
1650
|
+
const labels = Array.isArray(task.labels) ? task.labels.filter((label) => typeof label === "string") : [];
|
|
1651
|
+
const scope = firstStringList2(task.scope, labels.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length)));
|
|
1652
|
+
const validation = firstStringList2(task.validation, task.validators, labels.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length)));
|
|
1653
|
+
const roleLabel = labels.find((label) => label.startsWith("role:"));
|
|
1654
|
+
const role = typeof task.role === "string" && task.role.trim().length > 0 ? task.role.trim() : roleLabel ? roleLabel.slice("role:".length) : undefined;
|
|
1655
|
+
return {
|
|
1656
|
+
auto_synced: true,
|
|
1657
|
+
...typeof task.title === "string" ? { title: task.title } : {},
|
|
1658
|
+
...typeof task.status === "string" ? { status: task.status } : {},
|
|
1659
|
+
...typeof task.description === "string" ? { description: task.description } : {},
|
|
1660
|
+
...typeof task.acceptance_criteria === "string" ? { acceptance_criteria: task.acceptance_criteria } : typeof task.acceptanceCriteria === "string" ? { acceptance_criteria: task.acceptanceCriteria } : {},
|
|
1661
|
+
...role ? { role } : {},
|
|
1662
|
+
...scope.length > 0 ? { scope } : {},
|
|
1663
|
+
...validation.length > 0 ? { validation } : {},
|
|
1664
|
+
_rig: { taskSource: source }
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function firstStringList2(...candidates) {
|
|
1668
|
+
for (const candidate of candidates) {
|
|
1669
|
+
if (!Array.isArray(candidate)) {
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
const list = candidate.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
1673
|
+
if (list.length > 0) {
|
|
1674
|
+
return list;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return [];
|
|
1678
|
+
}
|
|
1679
|
+
function readConfiguredFilesTaskSourcePath2(projectRoot) {
|
|
1680
|
+
const jsonPath = resolve11(projectRoot, "rig.config.json");
|
|
1681
|
+
if (existsSync9(jsonPath)) {
|
|
1682
|
+
try {
|
|
1683
|
+
const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
|
|
1684
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1685
|
+
const taskSource = parsed.taskSource;
|
|
1686
|
+
if (taskSource && typeof taskSource === "object" && !Array.isArray(taskSource)) {
|
|
1687
|
+
const record = taskSource;
|
|
1688
|
+
return record.kind === "files" && typeof record.path === "string" ? record.path : null;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
} catch {
|
|
1692
|
+
return null;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
const tsPath = resolve11(projectRoot, "rig.config.ts");
|
|
1696
|
+
if (!existsSync9(tsPath)) {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
try {
|
|
1700
|
+
const source = readFileSync6(tsPath, "utf8");
|
|
1701
|
+
const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
|
|
1702
|
+
const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
|
|
1703
|
+
if (kind !== "files") {
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1706
|
+
return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
|
|
1707
|
+
} catch {
|
|
1708
|
+
return null;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
function sourceTaskConfigCandidates(projectRoot) {
|
|
1712
|
+
const runtimeContext = loadRuntimeContextFromEnv();
|
|
1713
|
+
return [
|
|
1714
|
+
runtimeContext?.monorepoMainRoot ? resolve11(runtimeContext.monorepoMainRoot, ".rig", "task-config.json") : "",
|
|
1715
|
+
process.env.MONOREPO_MAIN_ROOT?.trim() ? resolve11(process.env.MONOREPO_MAIN_ROOT.trim(), ".rig", "task-config.json") : "",
|
|
1716
|
+
resolve11(resolveMonorepoRoot2(projectRoot), ".rig", "task-config.json")
|
|
1717
|
+
].filter(Boolean);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// packages/runtime/src/binary-run.ts
|
|
1721
|
+
var runtimeBinaryBuildQueue = Promise.resolve();
|
|
1722
|
+
// packages/runtime/src/control-plane/provider/runtime-instructions.ts
|
|
1723
|
+
var CLAUDE_ROUTER_TOOL_NAMES = [
|
|
1724
|
+
"`mcp__rig_runtime_tools__read`",
|
|
1725
|
+
"`mcp__rig_runtime_tools__write`",
|
|
1726
|
+
"`mcp__rig_runtime_tools__edit`",
|
|
1727
|
+
"`mcp__rig_runtime_tools__glob`",
|
|
1728
|
+
"`mcp__rig_runtime_tools__grep`"
|
|
1729
|
+
].join(", ");
|
|
1730
|
+
var CODEX_DYNAMIC_TOOL_NAMES = [
|
|
1731
|
+
"`shell`",
|
|
1732
|
+
"`read`",
|
|
1733
|
+
"`write`",
|
|
1734
|
+
"`edit`",
|
|
1735
|
+
"`glob`",
|
|
1736
|
+
"`grep`"
|
|
1737
|
+
].join(", ");
|
|
1738
|
+
|
|
1739
|
+
// packages/runtime/src/control-plane/native/task-ops.ts
|
|
1740
|
+
var BUILD_CONFIG = readBuildConfig();
|
|
1741
|
+
var BAKED_INFO_OUTPUT = BUILD_CONFIG.AGENT_INFO_OUTPUT ?? "";
|
|
1742
|
+
var BAKED_DEPS_OUTPUT = BUILD_CONFIG.AGENT_DEPS_OUTPUT ?? "";
|
|
1743
|
+
var BAKED_STATUS_OUTPUT = BUILD_CONFIG.AGENT_STATUS_OUTPUT ?? "";
|
|
1744
|
+
var REOPENABLE_TASK_STATUSES = new Set(["completed", "cancelled", "blocked"]);
|
|
1745
|
+
var GENERATED_TASK_ARTIFACT_FILES = new Set([
|
|
1746
|
+
"changed-files.txt",
|
|
1747
|
+
"decision-log.md",
|
|
1748
|
+
"next-actions.md",
|
|
1749
|
+
"task-result.json",
|
|
1750
|
+
"validation-summary.json",
|
|
1751
|
+
"review-feedback.md",
|
|
1752
|
+
"review-state.json",
|
|
1753
|
+
"review-status.txt",
|
|
1754
|
+
"review-greptile-raw.json",
|
|
1755
|
+
"pr-state.json",
|
|
1756
|
+
"git-state.txt"
|
|
1757
|
+
]);
|
|
1758
|
+
|
|
1759
|
+
// packages/runtime/src/control-plane/native/git-ops.ts
|
|
1760
|
+
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
1761
|
+
"changed-files.txt",
|
|
1762
|
+
"contract-changes.md",
|
|
1763
|
+
"decision-log.md",
|
|
1764
|
+
"git-state.txt",
|
|
1765
|
+
"next-actions.md",
|
|
1766
|
+
"pr-state.json",
|
|
1767
|
+
"task-result.json",
|
|
1768
|
+
"validation-summary.json"
|
|
1769
|
+
]);
|
|
1770
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
1771
|
+
const path = resolve12(artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
1772
|
+
if (!existsSync10(path)) {
|
|
1773
|
+
return [];
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
const parsed = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1777
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1778
|
+
return [];
|
|
1779
|
+
}
|
|
1780
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
1781
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
1782
|
+
}
|
|
1783
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
1784
|
+
} catch {
|
|
1785
|
+
return [];
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
function isGitOpenPrResult(value) {
|
|
1789
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1790
|
+
return false;
|
|
1791
|
+
}
|
|
1792
|
+
const record = value;
|
|
1793
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
// packages/runtime/src/control-plane/native/verifier.ts
|
|
1797
|
+
async function verifyTask(options) {
|
|
1798
|
+
const paths = resolveHarnessPaths(options.projectRoot);
|
|
1799
|
+
const taskId = options.taskId;
|
|
1800
|
+
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
1801
|
+
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
1802
|
+
mkdirSync5(artifactDir, { recursive: true });
|
|
1803
|
+
const validationSummaryPath = resolve13(artifactDir, "validation-summary.json");
|
|
1804
|
+
const reviewFeedbackPath = resolve13(artifactDir, "review-feedback.md");
|
|
1805
|
+
const reviewStatePath = resolve13(artifactDir, "review-state.json");
|
|
1806
|
+
const greptileRawPath = resolve13(artifactDir, "review-greptile-raw.json");
|
|
1807
|
+
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
1808
|
+
const prState = prStates[0] || null;
|
|
1809
|
+
const localReasons = [];
|
|
1810
|
+
const aiReasons = [];
|
|
1811
|
+
const aiWarnings = [];
|
|
1812
|
+
let aiVerdict = "SKIP";
|
|
1813
|
+
let aiRawFeedback = "";
|
|
1814
|
+
const persistArtifacts = options.persistArtifacts !== false;
|
|
1815
|
+
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
1816
|
+
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
1817
|
+
}
|
|
1818
|
+
if (!existsSync11(validationSummaryPath)) {
|
|
1819
|
+
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
1820
|
+
} else {
|
|
1821
|
+
const summary = await parseValidationSummary(validationSummaryPath);
|
|
1822
|
+
if (!isAcceptedValidationSummary(summary)) {
|
|
1823
|
+
localReasons.push(`[Validation] validation-summary status is '${summary?.status ?? "unknown"}', expected 'pass' or zero-check 'skipped'.`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
1827
|
+
const requiredPath = resolve13(artifactDir, file);
|
|
1828
|
+
if (!existsSync11(requiredPath)) {
|
|
1829
|
+
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const taskResultPath = resolve13(artifactDir, "task-result.json");
|
|
1833
|
+
if (existsSync11(taskResultPath)) {
|
|
1834
|
+
const taskResult = await readJsonFile2(taskResultPath);
|
|
1835
|
+
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
1836
|
+
if (artifactStatus === "partial") {
|
|
1837
|
+
localReasons.push("[Artifact Quality] task-result.json status is 'partial'; completion requires a terminal result.");
|
|
1838
|
+
}
|
|
1839
|
+
if (hasNonEmptyBlockers(taskResult?.blockers)) {
|
|
1840
|
+
localReasons.push("[Artifact Quality] task-result.json blockers must be empty for completion.");
|
|
1841
|
+
}
|
|
1842
|
+
if (nextActionsIndicateRemainingScope(stringifyNextActions(taskResult?.nextActions ?? taskResult?.next_actions))) {
|
|
1843
|
+
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const nextActionsPath = resolve13(artifactDir, "next-actions.md");
|
|
1847
|
+
if (existsSync11(nextActionsPath)) {
|
|
1848
|
+
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
1849
|
+
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
1850
|
+
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
1851
|
+
}
|
|
1852
|
+
if (nextActionsIndicateRemainingScope(nextActionsContent)) {
|
|
1853
|
+
localReasons.push("[Artifact Quality] next-actions.md indicates remaining implementation scope.");
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const sourceCloseoutIssueId = resolveGithubSourceIssueId(options.projectRoot, taskId);
|
|
1857
|
+
if (sourceCloseoutIssueId) {
|
|
1858
|
+
localReasons.push(...evaluateGithubSourceIssuePrCloseout(options.projectRoot, prStates, sourceCloseoutIssueId));
|
|
1859
|
+
}
|
|
1860
|
+
const pluginResults = await options.plugins.runValidators(taskId);
|
|
1861
|
+
for (const result of pluginResults) {
|
|
1862
|
+
if (!result.passed) {
|
|
1863
|
+
localReasons.push(`[Plugin Validator] ${result.id}: ${result.summary}`);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
const reviewMode = await loadReviewMode(paths.reviewProfilePath, process.env.AI_REVIEW_MODE || "advisory");
|
|
1867
|
+
const reviewProvider = await loadReviewProvider(paths.reviewProfilePath, process.env.AI_REVIEW_PROVIDER || "greptile");
|
|
1868
|
+
if (!options.skipAiReview && localReasons.length === 0 && reviewProvider === "greptile" && reviewMode !== "off") {
|
|
1869
|
+
const ai = await runGreptileReview({
|
|
1870
|
+
projectRoot: options.projectRoot,
|
|
1871
|
+
taskId,
|
|
1872
|
+
artifactDir,
|
|
1873
|
+
prStates,
|
|
1874
|
+
reviewMode
|
|
1875
|
+
});
|
|
1876
|
+
aiVerdict = ai.verdict;
|
|
1877
|
+
aiRawFeedback = ai.feedback;
|
|
1878
|
+
aiReasons.push(...ai.reasons);
|
|
1879
|
+
aiWarnings.push(...ai.warnings);
|
|
1880
|
+
if (reviewMode === "required" && ai.verdict !== "APPROVE" && ai.reasons.length === 0) {
|
|
1881
|
+
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
1882
|
+
}
|
|
1883
|
+
if (persistArtifacts && ai.rawResponse) {
|
|
1884
|
+
writeFileSync6(greptileRawPath, `${ai.rawResponse}
|
|
1885
|
+
`, "utf-8");
|
|
1886
|
+
}
|
|
1887
|
+
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
1888
|
+
aiWarnings.push("[AI Review] AI review mode is off.");
|
|
1889
|
+
}
|
|
1890
|
+
const aiReviewApproved = options.skipAiReview || isAiReviewApproved({
|
|
1891
|
+
reviewMode,
|
|
1892
|
+
aiVerdict,
|
|
1893
|
+
aiReasons
|
|
1894
|
+
});
|
|
1895
|
+
if (persistArtifacts) {
|
|
1896
|
+
writeFeedbackFile({
|
|
1897
|
+
taskId,
|
|
1898
|
+
provider: reviewProvider,
|
|
1899
|
+
mode: reviewMode,
|
|
1900
|
+
verdict: aiVerdict,
|
|
1901
|
+
localReasons,
|
|
1902
|
+
aiReasons,
|
|
1903
|
+
aiWarnings,
|
|
1904
|
+
aiRawFeedback,
|
|
1905
|
+
output: reviewFeedbackPath
|
|
1906
|
+
});
|
|
1907
|
+
writeReviewStateFile({
|
|
1908
|
+
taskId,
|
|
1909
|
+
approved: localReasons.length === 0 && aiReviewApproved,
|
|
1910
|
+
provider: reviewProvider,
|
|
1911
|
+
mode: reviewMode,
|
|
1912
|
+
verdict: aiVerdict,
|
|
1913
|
+
prState,
|
|
1914
|
+
localReasons,
|
|
1915
|
+
aiReasons,
|
|
1916
|
+
aiWarnings,
|
|
1917
|
+
output: reviewStatePath
|
|
1918
|
+
});
|
|
1919
|
+
}
|
|
1920
|
+
if (localReasons.length > 0) {
|
|
1921
|
+
return {
|
|
1922
|
+
approved: false,
|
|
1923
|
+
localReasons,
|
|
1924
|
+
aiReasons,
|
|
1925
|
+
aiWarnings,
|
|
1926
|
+
aiVerdict,
|
|
1927
|
+
reviewFeedbackPath,
|
|
1928
|
+
reviewStatePath
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
if (!aiReviewApproved) {
|
|
1932
|
+
return {
|
|
1933
|
+
approved: false,
|
|
1934
|
+
localReasons,
|
|
1935
|
+
aiReasons,
|
|
1936
|
+
aiWarnings,
|
|
1937
|
+
aiVerdict,
|
|
1938
|
+
reviewFeedbackPath,
|
|
1939
|
+
reviewStatePath
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
return {
|
|
1943
|
+
approved: true,
|
|
1944
|
+
localReasons,
|
|
1945
|
+
aiReasons,
|
|
1946
|
+
aiWarnings,
|
|
1947
|
+
aiVerdict,
|
|
1948
|
+
reviewFeedbackPath,
|
|
1949
|
+
reviewStatePath
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
async function readJsonFile2(path) {
|
|
1953
|
+
try {
|
|
1954
|
+
return await Bun.file(path).json();
|
|
1955
|
+
} catch {
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function hasNonEmptyBlockers(blockers) {
|
|
1960
|
+
if (blockers == null) {
|
|
1961
|
+
return false;
|
|
1962
|
+
}
|
|
1963
|
+
if (typeof blockers === "string") {
|
|
1964
|
+
const normalized = blockers.trim().toLowerCase();
|
|
1965
|
+
return normalized.length > 0 && normalized !== "none" && normalized !== "n/a";
|
|
1966
|
+
}
|
|
1967
|
+
if (Array.isArray(blockers)) {
|
|
1968
|
+
return blockers.some((entry) => {
|
|
1969
|
+
if (typeof entry === "string") {
|
|
1970
|
+
const normalized = entry.trim().toLowerCase();
|
|
1971
|
+
return normalized.length > 0 && normalized !== "none" && normalized !== "n/a";
|
|
1972
|
+
}
|
|
1973
|
+
return entry != null;
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
if (typeof blockers === "object") {
|
|
1977
|
+
return Object.keys(blockers).length > 0;
|
|
1978
|
+
}
|
|
1979
|
+
return Boolean(blockers);
|
|
1980
|
+
}
|
|
1981
|
+
function stringifyNextActions(value) {
|
|
1982
|
+
if (value == null) {
|
|
1983
|
+
return "";
|
|
1984
|
+
}
|
|
1985
|
+
if (typeof value === "string") {
|
|
1986
|
+
return value;
|
|
1987
|
+
}
|
|
1988
|
+
if (Array.isArray(value)) {
|
|
1989
|
+
return value.map((entry) => stringifyNextActions(entry)).join(`
|
|
1990
|
+
`);
|
|
1991
|
+
}
|
|
1992
|
+
return JSON.stringify(value);
|
|
1993
|
+
}
|
|
1994
|
+
function nextActionsIndicateRemainingScope(content) {
|
|
1995
|
+
const normalized = content.trim();
|
|
1996
|
+
if (!normalized) {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
const lower = normalized.toLowerCase();
|
|
2000
|
+
if (/^(#\s*)?next actions\s*\n+\s*(none|n\/a|no remaining scope)\.?\s*$/i.test(normalized)) {
|
|
2001
|
+
return false;
|
|
2002
|
+
}
|
|
2003
|
+
return /^\s*- \[ \]/m.test(normalized) || /\b(remaining scope|still need|needs? to be implemented|not yet implemented|not implemented|follow[- ]?up required|blocked by|blocker:)\b/i.test(lower);
|
|
2004
|
+
}
|
|
2005
|
+
async function hasConfiguredSourceTask(projectRoot, taskId) {
|
|
2006
|
+
return readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
2007
|
+
}
|
|
2008
|
+
function resolveGithubSourceIssueId(projectRoot, taskId) {
|
|
2009
|
+
const fromRuntime = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
2010
|
+
if (typeof fromRuntime === "string" && isGithubSourceIssueId(fromRuntime)) {
|
|
2011
|
+
return fromRuntime;
|
|
2012
|
+
}
|
|
2013
|
+
try {
|
|
2014
|
+
const taskConfig = readTaskConfig(projectRoot);
|
|
2015
|
+
const entry = taskConfig[taskId];
|
|
2016
|
+
const sourceIssueId = typeof entry?.sourceIssueId === "string" ? entry.sourceIssueId : typeof entry?.source_issue_id === "string" ? entry.source_issue_id : null;
|
|
2017
|
+
if (sourceIssueId && isGithubSourceIssueId(sourceIssueId)) {
|
|
2018
|
+
return sourceIssueId;
|
|
2019
|
+
}
|
|
2020
|
+
const rig = entry?._rig;
|
|
2021
|
+
if (rig && typeof rig === "object" && !Array.isArray(rig)) {
|
|
2022
|
+
const rigRecord = rig;
|
|
2023
|
+
const rigSourceIssueId = typeof rigRecord.sourceIssueId === "string" ? rigRecord.sourceIssueId : null;
|
|
2024
|
+
if (rigSourceIssueId && isGithubSourceIssueId(rigSourceIssueId)) {
|
|
2025
|
+
return rigSourceIssueId;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
} catch {}
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
function isGithubSourceIssueId(value) {
|
|
2032
|
+
return /^[^/\s]+\/[^#\s]+#\d+$/.test(value.trim());
|
|
2033
|
+
}
|
|
2034
|
+
function evaluateGithubSourceIssuePrCloseout(projectRoot, prStates, sourceIssueId) {
|
|
2035
|
+
const issueNumber = sourceIssueId.match(/#(\d+)$/)?.[1];
|
|
2036
|
+
if (!issueNumber) {
|
|
2037
|
+
return [`[Source Issue] GitHub issue task ${sourceIssueId} has an invalid source issue id.`];
|
|
2038
|
+
}
|
|
2039
|
+
if (prStates.length === 0) {
|
|
2040
|
+
return [
|
|
2041
|
+
`[Source Issue] GitHub issue task ${sourceIssueId} must have an open, green, mergeable PR with resolved review feedback and closeout intent (for example "Closes #${issueNumber}"). Source issue closure or merge is not required before completion.`
|
|
2042
|
+
];
|
|
2043
|
+
}
|
|
2044
|
+
const rejectionReasons = [];
|
|
2045
|
+
for (const prState of prStates) {
|
|
2046
|
+
const hydratedPr = hydratePrCloseoutState(projectRoot, prState);
|
|
2047
|
+
const prReasons = evaluateSinglePrSourceIssueCloseout(hydratedPr, sourceIssueId, issueNumber);
|
|
2048
|
+
if (prReasons.length === 0) {
|
|
2049
|
+
return [];
|
|
2050
|
+
}
|
|
2051
|
+
rejectionReasons.push(...prReasons);
|
|
2052
|
+
}
|
|
2053
|
+
return uniqueStrings(rejectionReasons);
|
|
2054
|
+
}
|
|
2055
|
+
function hydratePrCloseoutState(projectRoot, prState) {
|
|
2056
|
+
if (hasLocalPrCloseoutEvidence(prState)) {
|
|
2057
|
+
return prState;
|
|
2058
|
+
}
|
|
2059
|
+
const snapshot = loadGithubPullRequestCloseoutSnapshot(projectRoot, prState);
|
|
2060
|
+
return snapshot ? { ...prState, ...snapshot } : prState;
|
|
2061
|
+
}
|
|
2062
|
+
function hasLocalPrCloseoutEvidence(prState) {
|
|
2063
|
+
const hasCloseoutText = typeof prState.title === "string" || typeof prState.body === "string" || Array.isArray(prState.closingIssues) && prState.closingIssues.length > 0;
|
|
2064
|
+
return hasCloseoutText && typeof prState.state === "string" && typeof prState.isDraft === "boolean" && typeof prState.mergeable === "string" && typeof prState.mergeStateStatus === "string" && typeof prState.reviewDecision === "string" && Array.isArray(prState.statusCheckRollup) && Array.isArray(prState.reviewThreads);
|
|
2065
|
+
}
|
|
2066
|
+
function loadGithubPullRequestCloseoutSnapshot(projectRoot, prState) {
|
|
2067
|
+
const prNumber = parsePullRequestNumber(prState.url || "");
|
|
2068
|
+
if (!prNumber) {
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
try {
|
|
2072
|
+
const repoName = deriveRepoName(projectRoot, prState);
|
|
2073
|
+
if (!repoName) {
|
|
2074
|
+
return null;
|
|
2075
|
+
}
|
|
2076
|
+
const view = runGhJson(projectRoot, [
|
|
2077
|
+
"pr",
|
|
2078
|
+
"view",
|
|
2079
|
+
`${prNumber}`,
|
|
2080
|
+
"--repo",
|
|
2081
|
+
repoName,
|
|
2082
|
+
"--json",
|
|
2083
|
+
"state,isDraft,mergeable,mergeStateStatus,reviewDecision,title,body,statusCheckRollup"
|
|
2084
|
+
]);
|
|
2085
|
+
return {
|
|
2086
|
+
state: stringField(view, "state"),
|
|
2087
|
+
isDraft: booleanField(view, "isDraft"),
|
|
2088
|
+
mergeable: stringField(view, "mergeable"),
|
|
2089
|
+
mergeStateStatus: stringField(view, "mergeStateStatus"),
|
|
2090
|
+
reviewDecision: stringField(view, "reviewDecision"),
|
|
2091
|
+
title: stringField(view, "title"),
|
|
2092
|
+
body: stringField(view, "body"),
|
|
2093
|
+
statusCheckRollup: statusCheckRollupField(view, "statusCheckRollup"),
|
|
2094
|
+
reviewThreads: loadGithubReviewThreads(projectRoot, repoName, prNumber)
|
|
2095
|
+
};
|
|
2096
|
+
} catch {
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
function evaluateSinglePrSourceIssueCloseout(prState, sourceIssueId, issueNumber) {
|
|
2101
|
+
const prLabel = prState.url || prState.branch || "recorded PR";
|
|
2102
|
+
const reasons = [];
|
|
2103
|
+
if (!prStateClosesIssue(prState, sourceIssueId, issueNumber)) {
|
|
2104
|
+
reasons.push(`[Source Issue] GitHub issue task ${sourceIssueId} must have PR closeout intent (for example "Closes #${issueNumber}"). "Closes part of #N" is not sufficient.`);
|
|
2105
|
+
}
|
|
2106
|
+
const state = (prState.state || "").toUpperCase();
|
|
2107
|
+
if (state !== "OPEN") {
|
|
2108
|
+
reasons.push(`[Source Issue] PR ${prLabel} must be open before operator-controlled merge; current state is ${prState.state || "unknown"}.`);
|
|
2109
|
+
}
|
|
2110
|
+
if (prState.isDraft !== false) {
|
|
2111
|
+
reasons.push(`[Source Issue] PR ${prLabel} must not be a draft.`);
|
|
2112
|
+
}
|
|
2113
|
+
const mergeable = (prState.mergeable || "").toUpperCase();
|
|
2114
|
+
const mergeStateStatus = (prState.mergeStateStatus || "").toUpperCase();
|
|
2115
|
+
if (mergeable !== "MERGEABLE" || mergeStateStatus === "DIRTY" || mergeStateStatus === "BLOCKED") {
|
|
2116
|
+
reasons.push(`[Source Issue] PR ${prLabel} must be mergeable before completion (mergeable=${prState.mergeable || "unknown"}, mergeStateStatus=${prState.mergeStateStatus || "unknown"}).`);
|
|
2117
|
+
}
|
|
2118
|
+
reasons.push(...evaluateSourceCloseoutChecks(prState, prLabel));
|
|
2119
|
+
reasons.push(...evaluateSourceCloseoutReviewState(prState, prLabel));
|
|
2120
|
+
return reasons;
|
|
2121
|
+
}
|
|
2122
|
+
function evaluateSourceCloseoutChecks(prState, prLabel) {
|
|
2123
|
+
const checks = prState.statusCheckRollup;
|
|
2124
|
+
if (!Array.isArray(checks) || checks.length === 0) {
|
|
2125
|
+
return [`[Source Issue] PR ${prLabel} must have green check evidence before completion.`];
|
|
2126
|
+
}
|
|
2127
|
+
const ciGate = evaluatePullRequestCiChecks(checks, "PR", 0);
|
|
2128
|
+
if (ciGate.verdict === "APPROVE") {
|
|
2129
|
+
return [];
|
|
2130
|
+
}
|
|
2131
|
+
return ciGate.reasons.map((reason) => reason.replace("PR#0", prLabel));
|
|
2132
|
+
}
|
|
2133
|
+
function evaluateSourceCloseoutReviewState(prState, prLabel) {
|
|
2134
|
+
const reasons = [];
|
|
2135
|
+
const reviewDecision = (prState.reviewDecision || "").toUpperCase();
|
|
2136
|
+
if (reviewDecision === "REVIEW_REQUIRED" || reviewDecision === "CHANGES_REQUESTED") {
|
|
2137
|
+
reasons.push(`[Source Issue] PR ${prLabel} required review is unresolved (${prState.reviewDecision}).`);
|
|
2138
|
+
}
|
|
2139
|
+
if (!Array.isArray(prState.reviewThreads)) {
|
|
2140
|
+
reasons.push(`[Source Issue] PR ${prLabel} review thread resolution could not be verified.`);
|
|
2141
|
+
return reasons;
|
|
2142
|
+
}
|
|
2143
|
+
for (const comment of filterUnresolvedReviewThreadComments(prState.reviewThreads)) {
|
|
2144
|
+
reasons.push(`[Source Issue] PR ${prLabel} has unresolved review thread on ${comment.path || "unknown path"}: ${summarizeComment(comment.body || "")}`);
|
|
2145
|
+
}
|
|
2146
|
+
return reasons;
|
|
2147
|
+
}
|
|
2148
|
+
function filterUnresolvedReviewThreadComments(threads) {
|
|
2149
|
+
const comments = [];
|
|
2150
|
+
for (const thread of threads) {
|
|
2151
|
+
if (thread.isResolved === true || thread.isOutdated === true) {
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
const threadComments = thread.comments?.nodes ?? [];
|
|
2155
|
+
if (threadComments.length === 0) {
|
|
2156
|
+
comments.push({ body: "Unresolved review thread" });
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
comments.push(threadComments[threadComments.length - 1]);
|
|
2160
|
+
}
|
|
2161
|
+
return comments;
|
|
2162
|
+
}
|
|
2163
|
+
function stringField(record, key) {
|
|
2164
|
+
const value = record[key];
|
|
2165
|
+
return typeof value === "string" ? value : undefined;
|
|
2166
|
+
}
|
|
2167
|
+
function booleanField(record, key) {
|
|
2168
|
+
const value = record[key];
|
|
2169
|
+
return typeof value === "boolean" ? value : undefined;
|
|
2170
|
+
}
|
|
2171
|
+
function statusCheckRollupField(record, key) {
|
|
2172
|
+
const value = record[key];
|
|
2173
|
+
if (!Array.isArray(value)) {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
return value.filter(isGithubStatusCheckRollupItem);
|
|
2177
|
+
}
|
|
2178
|
+
function isGithubStatusCheckRollupItem(value) {
|
|
2179
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2180
|
+
}
|
|
2181
|
+
function uniqueStrings(values) {
|
|
2182
|
+
return Array.from(new Set(values));
|
|
2183
|
+
}
|
|
2184
|
+
function prStateClosesIssue(pr, sourceIssueId, issueNumber) {
|
|
2185
|
+
const closingIssues = pr.closingIssues ?? [];
|
|
2186
|
+
if (closingIssues.some((issue) => closingIssueMatches(issue, sourceIssueId, issueNumber))) {
|
|
2187
|
+
return true;
|
|
2188
|
+
}
|
|
2189
|
+
const text = [pr.title, pr.body].filter((value) => typeof value === "string").join(`
|
|
2190
|
+
`);
|
|
2191
|
+
return prTextClosesIssue(text, issueNumber);
|
|
2192
|
+
}
|
|
2193
|
+
function closingIssueMatches(issue, sourceIssueId, issueNumber) {
|
|
2194
|
+
if (typeof issue === "number") {
|
|
2195
|
+
return String(issue) === issueNumber;
|
|
2196
|
+
}
|
|
2197
|
+
if (typeof issue === "string") {
|
|
2198
|
+
return issue === sourceIssueId || issue.replace(/^#/, "") === issueNumber;
|
|
2199
|
+
}
|
|
2200
|
+
return String(issue.number ?? "") === issueNumber || issue.id === issueNumber || issue.sourceIssueId === sourceIssueId;
|
|
2201
|
+
}
|
|
2202
|
+
function prTextClosesIssue(text, issueNumber) {
|
|
2203
|
+
if (!text.trim()) {
|
|
2204
|
+
return false;
|
|
2205
|
+
}
|
|
2206
|
+
const escaped = issueNumber.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2207
|
+
const closePattern = new RegExp(`\\b(close[sd]?|fix(e[sd])?|resolve[sd]?)\\s+#${escaped}\\b`, "i");
|
|
2208
|
+
return closePattern.test(text);
|
|
2209
|
+
}
|
|
2210
|
+
async function parseValidationSummary(path) {
|
|
2211
|
+
return readJsonFile2(path);
|
|
2212
|
+
}
|
|
2213
|
+
function isAcceptedValidationSummary(summary) {
|
|
2214
|
+
if (!summary) {
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
if (summary.status === "pass") {
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
2221
|
+
}
|
|
2222
|
+
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
2223
|
+
const parsed = existsSync11(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
2224
|
+
const mode = parsed?.mode;
|
|
2225
|
+
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
2226
|
+
return mode;
|
|
2227
|
+
}
|
|
2228
|
+
if (fallback === "off" || fallback === "advisory" || fallback === "required") {
|
|
2229
|
+
return fallback;
|
|
2230
|
+
}
|
|
2231
|
+
return "advisory";
|
|
2232
|
+
}
|
|
2233
|
+
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
2234
|
+
const parsed = existsSync11(reviewProfilePath) ? await readJsonFile2(reviewProfilePath) : null;
|
|
2235
|
+
const provider = parsed?.provider;
|
|
2236
|
+
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
2237
|
+
return provider;
|
|
2238
|
+
}
|
|
2239
|
+
return fallback || "greptile";
|
|
2240
|
+
}
|
|
2241
|
+
function resolveRepoSlug(projectRoot) {
|
|
2242
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
2243
|
+
const remote = runCapture(["git", "-C", paths.monorepoRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim() || runCapture(["git", "-C", projectRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim();
|
|
2244
|
+
if (!remote) {
|
|
2245
|
+
return "";
|
|
2246
|
+
}
|
|
2247
|
+
if (remote.startsWith("git@")) {
|
|
2248
|
+
return remote.split(":")[1]?.replace(/\.git$/, "") || "";
|
|
2249
|
+
}
|
|
2250
|
+
const cleaned = remote.replace(/^https?:\/\//, "").replace(/\.git$/, "");
|
|
2251
|
+
const slash = cleaned.indexOf("/");
|
|
2252
|
+
return slash >= 0 ? cleaned.slice(slash + 1) : cleaned;
|
|
2253
|
+
}
|
|
2254
|
+
async function runGreptileReview(options) {
|
|
2255
|
+
const reasons = [];
|
|
2256
|
+
const warnings = [];
|
|
2257
|
+
const secrets = resolveRuntimeSecrets(process.env);
|
|
2258
|
+
const apiKey = secrets.GREPTILE_API_KEY || "";
|
|
2259
|
+
const apiBase = secrets.GREPTILE_API_BASE || "https://api.greptile.com/mcp";
|
|
2260
|
+
const remote = secrets.GREPTILE_REMOTE || "github";
|
|
2261
|
+
const greptileDefaultBranch = secrets.GREPTILE_DEFAULT_BRANCH || "main";
|
|
2262
|
+
const { pollAttempts, pollIntervalMs } = resolveGreptilePollSettings({
|
|
2263
|
+
reviewMode: options.reviewMode,
|
|
2264
|
+
secrets
|
|
2265
|
+
});
|
|
2266
|
+
if (!apiKey) {
|
|
2267
|
+
reasons.push("[AI Review] Missing GREPTILE_API_KEY.");
|
|
2268
|
+
return { verdict: "SKIP", feedback: "", reasons, warnings, rawResponse: "" };
|
|
2269
|
+
}
|
|
2270
|
+
if (options.prStates.length === 0) {
|
|
2271
|
+
reasons.push("[AI Review] Missing pr-state.json or no PRs were recorded for review.");
|
|
2272
|
+
return { verdict: "SKIP", feedback: "", reasons, warnings, rawResponse: "" };
|
|
2273
|
+
}
|
|
2274
|
+
const perPrResults = [];
|
|
2275
|
+
for (const prState of options.prStates) {
|
|
2276
|
+
try {
|
|
2277
|
+
const prResult = await runGreptileReviewForPr({
|
|
2278
|
+
apiBase,
|
|
2279
|
+
apiKey,
|
|
2280
|
+
remote,
|
|
2281
|
+
defaultBranch: greptileDefaultBranch,
|
|
2282
|
+
projectRoot: options.projectRoot,
|
|
2283
|
+
taskId: options.taskId,
|
|
2284
|
+
prState,
|
|
2285
|
+
reviewMode: options.reviewMode,
|
|
2286
|
+
pollAttempts,
|
|
2287
|
+
pollIntervalMs
|
|
2288
|
+
});
|
|
2289
|
+
if (prResult.verdict === "SKIP") {
|
|
2290
|
+
warnings.push(...prResult.reasons.map(asGreptileInfrastructureWarning));
|
|
2291
|
+
} else {
|
|
2292
|
+
reasons.push(...prResult.reasons);
|
|
2293
|
+
}
|
|
2294
|
+
warnings.push(...prResult.warnings);
|
|
2295
|
+
perPrResults.push(prResult);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2298
|
+
const fallback = await runGithubGreptileFallbackReviewForPr({
|
|
2299
|
+
projectRoot: options.projectRoot,
|
|
2300
|
+
taskId: options.taskId,
|
|
2301
|
+
prState,
|
|
2302
|
+
reviewMode: options.reviewMode,
|
|
2303
|
+
infrastructureError: message,
|
|
2304
|
+
pollAttempts,
|
|
2305
|
+
pollIntervalMs
|
|
2306
|
+
}).catch(() => null);
|
|
2307
|
+
if (fallback) {
|
|
2308
|
+
if (fallback.verdict === "SKIP") {
|
|
2309
|
+
warnings.push(...fallback.reasons.map(asGreptileInfrastructureWarning));
|
|
2310
|
+
} else {
|
|
2311
|
+
reasons.push(...fallback.reasons);
|
|
2312
|
+
}
|
|
2313
|
+
warnings.push(...fallback.warnings);
|
|
2314
|
+
perPrResults.push(fallback);
|
|
2315
|
+
continue;
|
|
2316
|
+
}
|
|
2317
|
+
warnings.push(asGreptileInfrastructureWarning(`Greptile infrastructure failure for ${prState.url || prState.branch || options.taskId}: ${message}`));
|
|
2318
|
+
perPrResults.push({
|
|
2319
|
+
verdict: "SKIP",
|
|
2320
|
+
feedback: "",
|
|
2321
|
+
reasons: [],
|
|
2322
|
+
warnings: [],
|
|
2323
|
+
rawPayload: { pr: prState, error: message }
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
if (perPrResults.some((result) => result.verdict === "REJECT")) {
|
|
2328
|
+
return {
|
|
2329
|
+
verdict: "REJECT",
|
|
2330
|
+
feedback: perPrResults.map((result) => result.feedback).filter(Boolean).join(`
|
|
2331
|
+
|
|
2332
|
+
`),
|
|
2333
|
+
reasons,
|
|
2334
|
+
warnings,
|
|
2335
|
+
rawResponse: JSON.stringify(perPrResults.map((result) => result.rawPayload), null, 2)
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
if (perPrResults.every((result) => result.verdict === "APPROVE")) {
|
|
2339
|
+
return {
|
|
2340
|
+
verdict: "APPROVE",
|
|
2341
|
+
feedback: perPrResults.map((result) => result.feedback).filter(Boolean).join(`
|
|
2342
|
+
|
|
2343
|
+
`),
|
|
2344
|
+
reasons,
|
|
2345
|
+
warnings,
|
|
2346
|
+
rawResponse: JSON.stringify(perPrResults.map((result) => result.rawPayload), null, 2)
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
verdict: "SKIP",
|
|
2351
|
+
feedback: perPrResults.map((result) => result.feedback).filter(Boolean).join(`
|
|
2352
|
+
|
|
2353
|
+
`),
|
|
2354
|
+
reasons,
|
|
2355
|
+
warnings,
|
|
2356
|
+
rawResponse: JSON.stringify(perPrResults.map((result) => result.rawPayload), null, 2)
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function writeFeedbackFile(options) {
|
|
2360
|
+
const lines = [
|
|
2361
|
+
"# AI Review Feedback",
|
|
2362
|
+
"",
|
|
2363
|
+
`- Task: ${options.taskId}`,
|
|
2364
|
+
`- Provider: ${options.provider}`,
|
|
2365
|
+
`- Mode: ${options.mode}`,
|
|
2366
|
+
`- Verdict: ${options.verdict}`,
|
|
2367
|
+
""
|
|
2368
|
+
];
|
|
2369
|
+
if (options.localReasons.length > 0) {
|
|
2370
|
+
lines.push("## Local Rejection Reasons", "");
|
|
2371
|
+
for (const reason of options.localReasons) {
|
|
2372
|
+
lines.push(`- ${reason}`);
|
|
2373
|
+
}
|
|
2374
|
+
lines.push("");
|
|
2375
|
+
}
|
|
2376
|
+
if (options.aiReasons.length > 0) {
|
|
2377
|
+
lines.push("## AI Rejection Reasons", "");
|
|
2378
|
+
for (const reason of options.aiReasons) {
|
|
2379
|
+
lines.push(`- ${reason}`);
|
|
2380
|
+
}
|
|
2381
|
+
lines.push("");
|
|
2382
|
+
}
|
|
2383
|
+
if (options.aiWarnings.length > 0) {
|
|
2384
|
+
lines.push("## AI Warnings", "");
|
|
2385
|
+
for (const warning of options.aiWarnings) {
|
|
2386
|
+
lines.push(`- ${warning}`);
|
|
2387
|
+
}
|
|
2388
|
+
lines.push("");
|
|
2389
|
+
}
|
|
2390
|
+
if (options.aiRawFeedback) {
|
|
2391
|
+
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
2392
|
+
}
|
|
2393
|
+
writeFileSync6(options.output, `${lines.join(`
|
|
2394
|
+
`)}
|
|
2395
|
+
`, "utf-8");
|
|
2396
|
+
}
|
|
2397
|
+
function writeReviewStateFile(options) {
|
|
2398
|
+
const payload = {
|
|
2399
|
+
task_id: options.taskId,
|
|
2400
|
+
approved: options.approved,
|
|
2401
|
+
provider: options.provider,
|
|
2402
|
+
mode: options.mode,
|
|
2403
|
+
verdict: options.verdict,
|
|
2404
|
+
pr: options.prState,
|
|
2405
|
+
local_reasons: options.localReasons,
|
|
2406
|
+
ai_reasons: options.aiReasons,
|
|
2407
|
+
ai_warnings: options.aiWarnings,
|
|
2408
|
+
updated_at: nowIso()
|
|
2409
|
+
};
|
|
2410
|
+
writeFileSync6(options.output, `${JSON.stringify(payload, null, 2)}
|
|
2411
|
+
`, "utf-8");
|
|
2412
|
+
}
|
|
2413
|
+
async function runGreptileReviewForPr(options) {
|
|
2414
|
+
const reasons = [];
|
|
2415
|
+
const warnings = [];
|
|
2416
|
+
const repoName = deriveRepoName(options.projectRoot, options.prState);
|
|
2417
|
+
const prNumber = parsePullRequestNumber(options.prState.url || "");
|
|
2418
|
+
const defaultBranch = options.defaultBranch;
|
|
2419
|
+
const expectedHeadSha = resolvePrHeadSha(options.projectRoot, options.prState);
|
|
2420
|
+
if (!repoName) {
|
|
2421
|
+
reasons.push(`[AI Review] Could not resolve repository slug for ${options.prState.repoLabel || options.prState.target || "PR"}.`);
|
|
2422
|
+
return { verdict: "SKIP", feedback: "", reasons, warnings, rawPayload: { pr: options.prState } };
|
|
2423
|
+
}
|
|
2424
|
+
if (!prNumber) {
|
|
2425
|
+
reasons.push(`[AI Review] Could not parse PR number from ${options.prState.url || "missing PR URL"}.`);
|
|
2426
|
+
return { verdict: "SKIP", feedback: "", reasons, warnings, rawPayload: { pr: options.prState } };
|
|
2427
|
+
}
|
|
2428
|
+
const githubPrState = loadGithubPullRequestState(options.projectRoot, repoName, prNumber);
|
|
2429
|
+
if (shouldPreferGithubGreptileFallback(githubPrState)) {
|
|
2430
|
+
return runGithubGreptileFallbackReviewForPr({
|
|
2431
|
+
projectRoot: options.projectRoot,
|
|
2432
|
+
taskId: options.taskId,
|
|
2433
|
+
prState: options.prState,
|
|
2434
|
+
reviewMode: options.reviewMode,
|
|
2435
|
+
infrastructureError: undefined,
|
|
2436
|
+
pollAttempts: options.pollAttempts,
|
|
2437
|
+
pollIntervalMs: options.pollIntervalMs
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
const initialReviewsPayload = await callGreptileMcpToolJson(options.apiBase, options.apiKey, "list_code_reviews", {
|
|
2441
|
+
name: repoName,
|
|
2442
|
+
remote: options.remote,
|
|
2443
|
+
defaultBranch,
|
|
2444
|
+
prNumber,
|
|
2445
|
+
limit: 20
|
|
2446
|
+
});
|
|
2447
|
+
const existingReview = findGreptileReviewForHeadSha(initialReviewsPayload.codeReviews || [], expectedHeadSha);
|
|
2448
|
+
const shouldTrigger = shouldTriggerGreptileReview(existingReview, expectedHeadSha);
|
|
2449
|
+
let triggerStartedAt = Date.now();
|
|
2450
|
+
if (shouldTrigger) {
|
|
2451
|
+
triggerStartedAt = Date.now();
|
|
2452
|
+
await callGreptileMcpTool(options.apiBase, options.apiKey, "trigger_code_review", {
|
|
2453
|
+
name: repoName,
|
|
2454
|
+
remote: options.remote,
|
|
2455
|
+
defaultBranch,
|
|
2456
|
+
branch: options.prState.branch,
|
|
2457
|
+
prNumber
|
|
2458
|
+
}).catch((error) => {
|
|
2459
|
+
throw new Error(`Greptile trigger failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2460
|
+
});
|
|
2461
|
+
} else {
|
|
2462
|
+
const existingCreatedAt = Date.parse(existingReview?.createdAt || "");
|
|
2463
|
+
triggerStartedAt = Number.isFinite(existingCreatedAt) ? existingCreatedAt : Date.now();
|
|
2464
|
+
}
|
|
2465
|
+
let selectedReview = null;
|
|
2466
|
+
let reviewsPayload = initialReviewsPayload;
|
|
2467
|
+
let githubCheckRollup = [];
|
|
2468
|
+
let githubCheckState = { pending: false, completed: false };
|
|
2469
|
+
for (let attempt = 0;; attempt += 1) {
|
|
2470
|
+
const listPayload = attempt === 0 ? initialReviewsPayload : await callGreptileMcpToolJson(options.apiBase, options.apiKey, "list_code_reviews", {
|
|
2471
|
+
name: repoName,
|
|
2472
|
+
remote: options.remote,
|
|
2473
|
+
defaultBranch,
|
|
2474
|
+
prNumber,
|
|
2475
|
+
limit: 20
|
|
2476
|
+
});
|
|
2477
|
+
reviewsPayload = listPayload;
|
|
2478
|
+
selectedReview = pickRelevantCodeReview(listPayload.codeReviews || [], triggerStartedAt, expectedHeadSha);
|
|
2479
|
+
githubCheckRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
2480
|
+
githubCheckState = classifyGithubGreptileCheckState(githubCheckRollup);
|
|
2481
|
+
if (!shouldContinueGreptileMcpPolling({
|
|
2482
|
+
attempt,
|
|
2483
|
+
pollAttempts: options.pollAttempts,
|
|
2484
|
+
githubCheckState,
|
|
2485
|
+
selectedReview
|
|
2486
|
+
})) {
|
|
2487
|
+
break;
|
|
2488
|
+
}
|
|
2489
|
+
await Bun.sleep(options.pollIntervalMs);
|
|
2490
|
+
}
|
|
2491
|
+
const ciGate = evaluatePullRequestCiChecks(githubCheckRollup, repoName, prNumber);
|
|
2492
|
+
if (ciGate.verdict !== "APPROVE") {
|
|
2493
|
+
return {
|
|
2494
|
+
verdict: ciGate.verdict,
|
|
2495
|
+
feedback: "",
|
|
2496
|
+
reasons: ciGate.reasons,
|
|
2497
|
+
warnings,
|
|
2498
|
+
rawPayload: { pr: options.prState, codeReviews: reviewsPayload, checkRollup: githubCheckRollup }
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
if (!selectedReview) {
|
|
2502
|
+
reasons.push(`[AI Review] Greptile did not produce a review for ${repoName}#${prNumber}.`);
|
|
2503
|
+
return {
|
|
2504
|
+
verdict: "SKIP",
|
|
2505
|
+
feedback: "",
|
|
2506
|
+
reasons,
|
|
2507
|
+
warnings,
|
|
2508
|
+
rawPayload: { pr: options.prState, codeReviews: reviewsPayload }
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
if (selectedReview.status === "FAILED") {
|
|
2512
|
+
reasons.push(`[AI Review] Greptile review failed for ${repoName}#${prNumber}.`);
|
|
2513
|
+
return {
|
|
2514
|
+
verdict: "SKIP",
|
|
2515
|
+
feedback: "",
|
|
2516
|
+
reasons,
|
|
2517
|
+
warnings,
|
|
2518
|
+
rawPayload: { pr: options.prState, codeReviews: reviewsPayload, selectedReview }
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
if (selectedReview.status === "SKIPPED") {
|
|
2522
|
+
reasons.push(`[AI Review] Greptile skipped review for ${repoName}#${prNumber}.`);
|
|
2523
|
+
return {
|
|
2524
|
+
verdict: "SKIP",
|
|
2525
|
+
feedback: "",
|
|
2526
|
+
reasons,
|
|
2527
|
+
warnings,
|
|
2528
|
+
rawPayload: { pr: options.prState, codeReviews: reviewsPayload, selectedReview }
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
if (selectedReview.status !== "COMPLETED") {
|
|
2532
|
+
if (githubCheckState.completed) {
|
|
2533
|
+
return runGithubGreptileFallbackReviewForPr({
|
|
2534
|
+
projectRoot: options.projectRoot,
|
|
2535
|
+
taskId: options.taskId,
|
|
2536
|
+
prState: options.prState,
|
|
2537
|
+
reviewMode: options.reviewMode,
|
|
2538
|
+
infrastructureError: `Greptile MCP review stayed ${selectedReview.status} after the GitHub Greptile check completed.`,
|
|
2539
|
+
pollAttempts: options.pollAttempts,
|
|
2540
|
+
pollIntervalMs: options.pollIntervalMs
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
reasons.push(`[AI Review] Greptile review for ${repoName}#${prNumber} did not finish before timeout (last status: ${selectedReview.status}).`);
|
|
2544
|
+
return {
|
|
2545
|
+
verdict: "SKIP",
|
|
2546
|
+
feedback: "",
|
|
2547
|
+
reasons,
|
|
2548
|
+
warnings,
|
|
2549
|
+
rawPayload: { pr: options.prState, codeReviews: reviewsPayload, selectedReview }
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
const reviewDetails = await callGreptileMcpToolJson(options.apiBase, options.apiKey, "get_code_review", { codeReviewId: selectedReview.id });
|
|
2553
|
+
const commentsPayload = await callGreptileMcpToolJson(options.apiBase, options.apiKey, "list_merge_request_comments", {
|
|
2554
|
+
name: repoName,
|
|
2555
|
+
remote: options.remote,
|
|
2556
|
+
defaultBranch,
|
|
2557
|
+
prNumber,
|
|
2558
|
+
greptileGenerated: true,
|
|
2559
|
+
createdAfter: selectedReview.createdAt
|
|
2560
|
+
});
|
|
2561
|
+
const actionableComments = filterActionableGreptileComments(commentsPayload.comments || []);
|
|
2562
|
+
const reviewBody = reviewDetails.codeReview?.body || "";
|
|
2563
|
+
const score = parseGreptileScore(reviewBody);
|
|
2564
|
+
const feedback = [
|
|
2565
|
+
`## ${options.prState.repoLabel || repoName} PR Review`,
|
|
2566
|
+
"",
|
|
2567
|
+
`- PR: ${options.prState.url || `${repoName}#${prNumber}`}`,
|
|
2568
|
+
`- Review ID: ${selectedReview.id}`,
|
|
2569
|
+
`- Status: ${selectedReview.status}`,
|
|
2570
|
+
"",
|
|
2571
|
+
reviewBody ? stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
2572
|
+
].filter(Boolean).join(`
|
|
2573
|
+
`);
|
|
2574
|
+
if (actionableComments.length > 0) {
|
|
2575
|
+
for (const comment of actionableComments) {
|
|
2576
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.filePath}: ${summarizeComment(comment.body || "")}`);
|
|
2577
|
+
}
|
|
2578
|
+
return {
|
|
2579
|
+
verdict: "REJECT",
|
|
2580
|
+
feedback,
|
|
2581
|
+
reasons,
|
|
2582
|
+
warnings,
|
|
2583
|
+
rawPayload: {
|
|
2584
|
+
pr: options.prState,
|
|
2585
|
+
codeReviews: reviewsPayload,
|
|
2586
|
+
selectedReview,
|
|
2587
|
+
reviewDetails,
|
|
2588
|
+
comments: commentsPayload
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
if (/not safe to merge|unsafe to merge|do not merge|blocker/i.test(reviewBody)) {
|
|
2593
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
2594
|
+
return {
|
|
2595
|
+
verdict: "REJECT",
|
|
2596
|
+
feedback,
|
|
2597
|
+
reasons,
|
|
2598
|
+
warnings,
|
|
2599
|
+
rawPayload: {
|
|
2600
|
+
pr: options.prState,
|
|
2601
|
+
codeReviews: reviewsPayload,
|
|
2602
|
+
selectedReview,
|
|
2603
|
+
reviewDetails,
|
|
2604
|
+
comments: commentsPayload
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
if (score) {
|
|
2609
|
+
if (score.scale === 5 && score.value < 5 && options.reviewMode === "required") {
|
|
2610
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; required mode needs 5/5 before merge.`);
|
|
2611
|
+
return {
|
|
2612
|
+
verdict: "REJECT",
|
|
2613
|
+
feedback,
|
|
2614
|
+
reasons,
|
|
2615
|
+
warnings,
|
|
2616
|
+
rawPayload: {
|
|
2617
|
+
pr: options.prState,
|
|
2618
|
+
codeReviews: reviewsPayload,
|
|
2619
|
+
selectedReview,
|
|
2620
|
+
reviewDetails,
|
|
2621
|
+
comments: commentsPayload,
|
|
2622
|
+
score
|
|
2623
|
+
}
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
if (score.scale === 5 && score.value <= 2) {
|
|
2627
|
+
reasons.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; this requires rework before merge.`);
|
|
2628
|
+
return {
|
|
2629
|
+
verdict: "REJECT",
|
|
2630
|
+
feedback,
|
|
2631
|
+
reasons,
|
|
2632
|
+
warnings,
|
|
2633
|
+
rawPayload: {
|
|
2634
|
+
pr: options.prState,
|
|
2635
|
+
codeReviews: reviewsPayload,
|
|
2636
|
+
selectedReview,
|
|
2637
|
+
reviewDetails,
|
|
2638
|
+
comments: commentsPayload,
|
|
2639
|
+
score
|
|
2640
|
+
}
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
if (score.scale === 5 && score.value < 5) {
|
|
2644
|
+
warnings.push(`[AI Review] ${repoName}#${prNumber} completed with Greptile confidence ${score.value}/${score.scale}; continue only after reviewing remaining risk.`);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
return {
|
|
2648
|
+
verdict: "APPROVE",
|
|
2649
|
+
feedback,
|
|
2650
|
+
reasons,
|
|
2651
|
+
warnings,
|
|
2652
|
+
rawPayload: {
|
|
2653
|
+
pr: options.prState,
|
|
2654
|
+
codeReviews: reviewsPayload,
|
|
2655
|
+
selectedReview,
|
|
2656
|
+
reviewDetails,
|
|
2657
|
+
comments: commentsPayload
|
|
2658
|
+
}
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
async function runGithubGreptileFallbackReviewForPr(options) {
|
|
2662
|
+
const repoName = deriveRepoName(options.projectRoot, options.prState);
|
|
2663
|
+
const prNumber = parsePullRequestNumber(options.prState.url || "");
|
|
2664
|
+
const expectedHeadSha = resolvePrHeadSha(options.projectRoot, options.prState);
|
|
2665
|
+
if (!repoName || !prNumber) {
|
|
2666
|
+
return {
|
|
2667
|
+
verdict: "SKIP",
|
|
2668
|
+
feedback: "",
|
|
2669
|
+
reasons: [],
|
|
2670
|
+
warnings: buildGithubGreptileFallbackWarnings(options),
|
|
2671
|
+
rawPayload: buildGithubGreptileFallbackRawPayload(options)
|
|
2672
|
+
};
|
|
2673
|
+
}
|
|
2674
|
+
let reviews = [];
|
|
2675
|
+
let selectedReview = null;
|
|
2676
|
+
let latestReview = null;
|
|
2677
|
+
let fallbackReview = null;
|
|
2678
|
+
let threads = [];
|
|
2679
|
+
let actionableThreads = [];
|
|
2680
|
+
let checkRollup = [];
|
|
2681
|
+
let checkState = { pending: false, completed: false };
|
|
2682
|
+
for (let attempt = 0;; attempt += 1) {
|
|
2683
|
+
reviews = runGhJson(options.projectRoot, ["api", `repos/${repoName}/pulls/${prNumber}/reviews`]);
|
|
2684
|
+
selectedReview = pickRelevantGithubGreptileReview(reviews, expectedHeadSha);
|
|
2685
|
+
latestReview = pickLatestGithubGreptileReview(reviews);
|
|
2686
|
+
fallbackReview = selectedReview ?? latestReview;
|
|
2687
|
+
threads = loadGithubReviewThreads(options.projectRoot, repoName, prNumber);
|
|
2688
|
+
actionableThreads = filterActionableGithubGreptileThreads(threads);
|
|
2689
|
+
checkRollup = loadGithubPullRequestCheckRollup(options.projectRoot, repoName, prNumber);
|
|
2690
|
+
checkState = classifyGithubGreptileCheckState(checkRollup);
|
|
2691
|
+
const approvedViaReviewedAncestor2 = !selectedReview && !!fallbackReview?.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
2692
|
+
if (!shouldContinueGithubGreptileFallbackPolling({
|
|
2693
|
+
attempt,
|
|
2694
|
+
pollAttempts: options.pollAttempts,
|
|
2695
|
+
checkState,
|
|
2696
|
+
fallbackReview,
|
|
2697
|
+
selectedReview,
|
|
2698
|
+
approvedViaReviewedAncestor: approvedViaReviewedAncestor2
|
|
2699
|
+
})) {
|
|
2700
|
+
break;
|
|
2701
|
+
}
|
|
2702
|
+
await Bun.sleep(options.pollIntervalMs);
|
|
2703
|
+
}
|
|
2704
|
+
const ciGate = evaluatePullRequestCiChecks(checkRollup, repoName, prNumber);
|
|
2705
|
+
if (ciGate.verdict !== "APPROVE") {
|
|
2706
|
+
return {
|
|
2707
|
+
verdict: ciGate.verdict,
|
|
2708
|
+
feedback: "",
|
|
2709
|
+
reasons: ciGate.reasons,
|
|
2710
|
+
warnings: buildGithubGreptileFallbackWarnings(options),
|
|
2711
|
+
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
const feedback = [
|
|
2715
|
+
`## ${options.prState.repoLabel || repoName} PR Review`,
|
|
2716
|
+
"",
|
|
2717
|
+
`- PR: ${options.prState.url || `${repoName}#${prNumber}`}`,
|
|
2718
|
+
"- Source: GitHub Greptile fallback",
|
|
2719
|
+
fallbackReview?.html_url ? `- Review: ${fallbackReview.html_url}` : "",
|
|
2720
|
+
fallbackReview?.state ? `- Status: ${fallbackReview.state}` : "",
|
|
2721
|
+
"",
|
|
2722
|
+
fallbackReview?.body?.trim() ? stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
|
|
2723
|
+
].filter(Boolean).join(`
|
|
2724
|
+
`);
|
|
2725
|
+
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
2726
|
+
if (checkState.pending) {
|
|
2727
|
+
return {
|
|
2728
|
+
verdict: "SKIP",
|
|
2729
|
+
feedback,
|
|
2730
|
+
reasons: [
|
|
2731
|
+
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is still in progress.`
|
|
2732
|
+
],
|
|
2733
|
+
warnings,
|
|
2734
|
+
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
const approvedViaCompletedCheck = isGithubGreptileCheckApproved(checkRollup);
|
|
2738
|
+
if (!fallbackReview) {
|
|
2739
|
+
if (approvedViaCompletedCheck) {
|
|
2740
|
+
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no GitHub Greptile review object, but the Greptile check completed successfully and no unresolved Greptile threads remain.`);
|
|
2741
|
+
return {
|
|
2742
|
+
verdict: "APPROVE",
|
|
2743
|
+
feedback,
|
|
2744
|
+
reasons: [],
|
|
2745
|
+
warnings,
|
|
2746
|
+
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
return {
|
|
2750
|
+
verdict: "SKIP",
|
|
2751
|
+
feedback,
|
|
2752
|
+
reasons: [
|
|
2753
|
+
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available.`
|
|
2754
|
+
],
|
|
2755
|
+
warnings,
|
|
2756
|
+
rawPayload: { ...buildGithubGreptileFallbackRawPayload(options), reviews, threads, checkRollup }
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
const approvedViaReviewedAncestor = !selectedReview && !!fallbackReview.commit_id && !!expectedHeadSha && isCommitAncestorOfPrHead(options.projectRoot, options.prState, fallbackReview.commit_id, expectedHeadSha);
|
|
2760
|
+
if (actionableThreads.length > 0) {
|
|
2761
|
+
return {
|
|
2762
|
+
verdict: "REJECT",
|
|
2763
|
+
feedback,
|
|
2764
|
+
reasons: actionableThreads.map((comment) => `[AI Review] ${repoName}#${prNumber} has unresolved Greptile comment on ${comment.path}: ${summarizeComment(comment.body || "")}`),
|
|
2765
|
+
warnings,
|
|
2766
|
+
rawPayload: {
|
|
2767
|
+
pr: options.prState,
|
|
2768
|
+
selectedReview: fallbackReview,
|
|
2769
|
+
reviews,
|
|
2770
|
+
threads,
|
|
2771
|
+
checkRollup,
|
|
2772
|
+
actionableThreads,
|
|
2773
|
+
...buildGithubGreptileFallbackRawPayload(options)
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
if (!selectedReview && !approvedViaReviewedAncestor) {
|
|
2778
|
+
if (approvedViaCompletedCheck) {
|
|
2779
|
+
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile GitHub review on the current head, but the Greptile check completed successfully and all Greptile threads are resolved.`);
|
|
2780
|
+
return {
|
|
2781
|
+
verdict: "APPROVE",
|
|
2782
|
+
feedback,
|
|
2783
|
+
reasons: [],
|
|
2784
|
+
warnings,
|
|
2785
|
+
rawPayload: {
|
|
2786
|
+
pr: options.prState,
|
|
2787
|
+
selectedReview: fallbackReview,
|
|
2788
|
+
reviews,
|
|
2789
|
+
threads,
|
|
2790
|
+
checkRollup,
|
|
2791
|
+
...buildGithubGreptileFallbackRawPayload(options)
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
return {
|
|
2796
|
+
verdict: "SKIP",
|
|
2797
|
+
feedback,
|
|
2798
|
+
reasons: [
|
|
2799
|
+
`[AI Review] Greptile GitHub review for ${repoName}#${prNumber} is not available for the current head.`
|
|
2800
|
+
],
|
|
2801
|
+
warnings,
|
|
2802
|
+
rawPayload: {
|
|
2803
|
+
pr: options.prState,
|
|
2804
|
+
selectedReview: fallbackReview,
|
|
2805
|
+
reviews,
|
|
2806
|
+
threads,
|
|
2807
|
+
checkRollup,
|
|
2808
|
+
...buildGithubGreptileFallbackRawPayload(options)
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
if (approvedViaReviewedAncestor) {
|
|
2813
|
+
warnings.push(`[AI Review Warning] ${repoName}#${prNumber} has no fresh Greptile review on the current head, but the latest reviewed commit is an ancestor and all Greptile threads are resolved.`);
|
|
2814
|
+
}
|
|
2815
|
+
return {
|
|
2816
|
+
verdict: "APPROVE",
|
|
2817
|
+
feedback,
|
|
2818
|
+
reasons: [],
|
|
2819
|
+
warnings,
|
|
2820
|
+
rawPayload: {
|
|
2821
|
+
pr: options.prState,
|
|
2822
|
+
selectedReview: fallbackReview,
|
|
2823
|
+
reviews,
|
|
2824
|
+
threads,
|
|
2825
|
+
checkRollup,
|
|
2826
|
+
...buildGithubGreptileFallbackRawPayload(options)
|
|
2827
|
+
}
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function buildGithubGreptileFallbackWarnings(options) {
|
|
2831
|
+
if (!options.infrastructureError?.trim()) {
|
|
2832
|
+
return [];
|
|
2833
|
+
}
|
|
2834
|
+
return [
|
|
2835
|
+
asGreptileInfrastructureWarning(`Greptile infrastructure failure for ${options.prState.url || options.prState.branch || options.taskId}: ${options.infrastructureError}`)
|
|
2836
|
+
];
|
|
2837
|
+
}
|
|
2838
|
+
function buildGithubGreptileFallbackRawPayload(options) {
|
|
2839
|
+
return options.infrastructureError?.trim() ? { pr: options.prState, error: options.infrastructureError } : { pr: options.prState };
|
|
2840
|
+
}
|
|
2841
|
+
async function callGreptileMcpTool(apiBase, apiKey, name, args) {
|
|
2842
|
+
return callGreptileMcpToolWithTimeout(apiBase, apiKey, name, args, resolveGreptileRequestTimeoutMs(process.env.GREPTILE_REQUEST_TIMEOUT_MS));
|
|
2843
|
+
}
|
|
2844
|
+
async function callGreptileMcpToolWithTimeout(apiBase, apiKey, name, args, timeoutMs) {
|
|
2845
|
+
const controller = new AbortController;
|
|
2846
|
+
const timeoutId = setTimeout(() => {
|
|
2847
|
+
controller.abort(new Error(`Greptile MCP tool ${name} timed out after ${timeoutMs}ms.`));
|
|
2848
|
+
}, timeoutMs);
|
|
2849
|
+
let response;
|
|
2850
|
+
try {
|
|
2851
|
+
response = await fetch(apiBase, {
|
|
2852
|
+
method: "POST",
|
|
2853
|
+
headers: {
|
|
2854
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2855
|
+
"Content-Type": "application/json"
|
|
2856
|
+
},
|
|
2857
|
+
body: JSON.stringify({
|
|
2858
|
+
jsonrpc: "2.0",
|
|
2859
|
+
id: `rig-${name}-${Date.now()}`,
|
|
2860
|
+
method: "tools/call",
|
|
2861
|
+
params: { name, arguments: args }
|
|
2862
|
+
}),
|
|
2863
|
+
signal: controller.signal
|
|
2864
|
+
});
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
if (controller.signal.aborted) {
|
|
2867
|
+
throw controller.signal.reason instanceof Error ? controller.signal.reason : new Error(`Greptile MCP tool ${name} timed out after ${timeoutMs}ms.`);
|
|
2868
|
+
}
|
|
2869
|
+
throw error;
|
|
2870
|
+
} finally {
|
|
2871
|
+
clearTimeout(timeoutId);
|
|
2872
|
+
}
|
|
2873
|
+
const raw = await response.text();
|
|
2874
|
+
if (!response.ok) {
|
|
2875
|
+
throw new Error(`HTTP ${response.status}: ${raw}`);
|
|
2876
|
+
}
|
|
2877
|
+
let envelope;
|
|
2878
|
+
try {
|
|
2879
|
+
envelope = JSON.parse(raw);
|
|
2880
|
+
} catch {
|
|
2881
|
+
throw new Error(`Malformed MCP response: ${raw}`);
|
|
2882
|
+
}
|
|
2883
|
+
if (envelope.error?.message) {
|
|
2884
|
+
throw new Error(envelope.error.message);
|
|
2885
|
+
}
|
|
2886
|
+
const text = (envelope.result?.content || []).filter((item) => item.type === "text" && typeof item.text === "string").map((item) => item.text || "").join(`
|
|
2887
|
+
`).trim();
|
|
2888
|
+
if (!text) {
|
|
2889
|
+
throw new Error(`MCP tool ${name} returned no text payload.`);
|
|
2890
|
+
}
|
|
2891
|
+
return text;
|
|
2892
|
+
}
|
|
2893
|
+
function resolveGreptileRequestTimeoutMs(rawValue) {
|
|
2894
|
+
const DEFAULT_GREPTILE_REQUEST_TIMEOUT_MS = 30000;
|
|
2895
|
+
const parsed = Number.parseInt(rawValue || `${DEFAULT_GREPTILE_REQUEST_TIMEOUT_MS}`, 10);
|
|
2896
|
+
return Number.isFinite(parsed) && parsed >= 1000 ? parsed : DEFAULT_GREPTILE_REQUEST_TIMEOUT_MS;
|
|
2897
|
+
}
|
|
2898
|
+
async function callGreptileMcpToolJson(apiBase, apiKey, name, args) {
|
|
2899
|
+
const text = await callGreptileMcpTool(apiBase, apiKey, name, args);
|
|
2900
|
+
try {
|
|
2901
|
+
return JSON.parse(text);
|
|
2902
|
+
} catch {
|
|
2903
|
+
throw new Error(`MCP tool ${name} returned malformed JSON: ${text}`);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
function pickRelevantCodeReview(reviews, triggerStartedAt, expectedHeadSha) {
|
|
2907
|
+
const normalized = [...reviews].sort((a, b) => Date.parse(b.createdAt || "") - Date.parse(a.createdAt || ""));
|
|
2908
|
+
const exactSha = normalized.find((review) => expectedHeadSha && review.metadata?.checkHeadSha === expectedHeadSha);
|
|
2909
|
+
if (exactSha) {
|
|
2910
|
+
return exactSha;
|
|
2911
|
+
}
|
|
2912
|
+
const triggered = normalized.find((review) => Date.parse(review.createdAt || "") >= triggerStartedAt - 1000);
|
|
2913
|
+
return triggered || normalized[0] || null;
|
|
2914
|
+
}
|
|
2915
|
+
function filterActionableGreptileComments(comments) {
|
|
2916
|
+
return comments.filter((comment) => comment.sourceType === "greptile" && typeof comment.filePath === "string" && comment.filePath.trim().length > 0 && comment.addressed === false);
|
|
2917
|
+
}
|
|
2918
|
+
function findGreptileReviewForHeadSha(reviews, expectedHeadSha) {
|
|
2919
|
+
if (!expectedHeadSha) {
|
|
2920
|
+
return null;
|
|
2921
|
+
}
|
|
2922
|
+
const normalized = [...reviews].sort((a, b) => Date.parse(b.createdAt || "") - Date.parse(a.createdAt || ""));
|
|
2923
|
+
return normalized.find((review) => review.metadata?.checkHeadSha === expectedHeadSha) || null;
|
|
2924
|
+
}
|
|
2925
|
+
function isGreptileReviewTerminal(status) {
|
|
2926
|
+
return status === "COMPLETED" || status === "FAILED" || status === "SKIPPED";
|
|
2927
|
+
}
|
|
2928
|
+
function shouldTriggerGreptileReview(existingReview, expectedHeadSha) {
|
|
2929
|
+
if (!existingReview) {
|
|
2930
|
+
return true;
|
|
2931
|
+
}
|
|
2932
|
+
if (!expectedHeadSha) {
|
|
2933
|
+
return true;
|
|
2934
|
+
}
|
|
2935
|
+
if ((existingReview.metadata?.checkHeadSha || "") !== expectedHeadSha) {
|
|
2936
|
+
return true;
|
|
2937
|
+
}
|
|
2938
|
+
return isGreptileReviewTerminal(existingReview.status);
|
|
2939
|
+
}
|
|
2940
|
+
function shouldContinueGreptileMcpPolling(options) {
|
|
2941
|
+
if (options.githubCheckState.completed) {
|
|
2942
|
+
return false;
|
|
2943
|
+
}
|
|
2944
|
+
if (options.selectedReview && !isGreptileReviewTerminal(options.selectedReview.status)) {
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
return options.attempt + 1 < options.pollAttempts;
|
|
2948
|
+
}
|
|
2949
|
+
function shouldContinueGithubGreptileFallbackPolling(options) {
|
|
2950
|
+
const waitingForVisiblePendingReview = options.checkState.pending && (!options.fallbackReview || !options.selectedReview && !options.approvedViaReviewedAncestor);
|
|
2951
|
+
if (waitingForVisiblePendingReview) {
|
|
2952
|
+
return true;
|
|
2953
|
+
}
|
|
2954
|
+
const reviewNotVisibleYet = !options.fallbackReview && !options.checkState.pending && !options.checkState.completed;
|
|
2955
|
+
if (reviewNotVisibleYet) {
|
|
2956
|
+
return options.attempt + 1 < options.pollAttempts;
|
|
2957
|
+
}
|
|
2958
|
+
return false;
|
|
2959
|
+
}
|
|
2960
|
+
function resolveGreptilePollSettings(options) {
|
|
2961
|
+
const DEFAULT_POLL_ATTEMPTS = 60;
|
|
2962
|
+
const DEFAULT_POLL_INTERVAL_MS = 5000;
|
|
2963
|
+
const REQUIRED_MODE_MIN_ATTEMPTS = 180;
|
|
2964
|
+
const configuredAttempts = Number.parseInt(options.secrets.GREPTILE_POLL_ATTEMPTS || `${DEFAULT_POLL_ATTEMPTS}`, 10);
|
|
2965
|
+
const configuredIntervalMs = Number.parseInt(options.secrets.GREPTILE_POLL_INTERVAL_MS || `${DEFAULT_POLL_INTERVAL_MS}`, 10);
|
|
2966
|
+
const pollAttempts = Number.isFinite(configuredAttempts) && configuredAttempts > 0 ? configuredAttempts : DEFAULT_POLL_ATTEMPTS;
|
|
2967
|
+
const pollIntervalMs = Number.isFinite(configuredIntervalMs) && configuredIntervalMs > 0 ? configuredIntervalMs : DEFAULT_POLL_INTERVAL_MS;
|
|
2968
|
+
if (options.reviewMode !== "required") {
|
|
2969
|
+
return { pollAttempts, pollIntervalMs };
|
|
2970
|
+
}
|
|
2971
|
+
return {
|
|
2972
|
+
pollAttempts: Math.max(REQUIRED_MODE_MIN_ATTEMPTS, pollAttempts),
|
|
2973
|
+
pollIntervalMs: Math.max(DEFAULT_POLL_INTERVAL_MS, pollIntervalMs)
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
function loadGithubPullRequestState(projectRoot, repoName, prNumber) {
|
|
2977
|
+
const response = runGhJson(projectRoot, [
|
|
2978
|
+
"api",
|
|
2979
|
+
`repos/${repoName}/pulls/${prNumber}`
|
|
2980
|
+
]);
|
|
2981
|
+
return {
|
|
2982
|
+
state: response.state || "",
|
|
2983
|
+
merged: response.merged,
|
|
2984
|
+
merged_at: response.merged_at ?? null
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
function shouldPreferGithubGreptileFallback(prState) {
|
|
2988
|
+
if ((prState?.state || "").toUpperCase() === "MERGED") {
|
|
2989
|
+
return true;
|
|
2990
|
+
}
|
|
2991
|
+
if (prState?.merged === true) {
|
|
2992
|
+
return true;
|
|
2993
|
+
}
|
|
2994
|
+
return typeof prState?.merged_at === "string" && prState.merged_at.trim().length > 0;
|
|
2995
|
+
}
|
|
2996
|
+
function parsePullRequestNumber(url) {
|
|
2997
|
+
const match = /\/pull\/(\d+)(?:\/|$)/.exec(url);
|
|
2998
|
+
return match ? Number.parseInt(match[1] || "0", 10) : 0;
|
|
2999
|
+
}
|
|
3000
|
+
function runGhJson(projectRoot, args) {
|
|
3001
|
+
const result = runCapture(["gh", ...args], projectRoot);
|
|
3002
|
+
if (result.exitCode !== 0) {
|
|
3003
|
+
throw new Error(result.stderr || result.stdout || `gh ${args.join(" ")} failed`);
|
|
3004
|
+
}
|
|
3005
|
+
try {
|
|
3006
|
+
return JSON.parse(result.stdout);
|
|
3007
|
+
} catch {
|
|
3008
|
+
throw new Error(`gh ${args.join(" ")} returned malformed JSON: ${result.stdout}`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function deriveRepoName(projectRoot, prState) {
|
|
3012
|
+
const fromUrl = /github\.com\/([^/]+\/[^/]+)\/pull\/\d+/.exec(prState.url || "");
|
|
3013
|
+
if (fromUrl?.[1]) {
|
|
3014
|
+
return fromUrl[1];
|
|
3015
|
+
}
|
|
3016
|
+
if (prState.target === "monorepo") {
|
|
3017
|
+
return resolveRepoSlug(projectRoot);
|
|
3018
|
+
}
|
|
3019
|
+
return runCapture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
|
|
3020
|
+
}
|
|
3021
|
+
function resolvePrHeadSha(projectRoot, prState) {
|
|
3022
|
+
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3023
|
+
return runCapture(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
3024
|
+
}
|
|
3025
|
+
function isGreptileGithubLogin(login) {
|
|
3026
|
+
return (login || "").replace(/\[bot\]$/, "") === "greptile-apps";
|
|
3027
|
+
}
|
|
3028
|
+
function pickRelevantGithubGreptileReview(reviews, expectedHeadSha) {
|
|
3029
|
+
const matching = sortGithubGreptileReviews(reviews);
|
|
3030
|
+
if (expectedHeadSha) {
|
|
3031
|
+
const exact = matching.find((review) => review.commit_id === expectedHeadSha);
|
|
3032
|
+
if (exact) {
|
|
3033
|
+
return exact;
|
|
3034
|
+
}
|
|
3035
|
+
return null;
|
|
3036
|
+
}
|
|
3037
|
+
return matching[0] || null;
|
|
3038
|
+
}
|
|
3039
|
+
function pickLatestGithubGreptileReview(reviews) {
|
|
3040
|
+
return sortGithubGreptileReviews(reviews)[0] || null;
|
|
3041
|
+
}
|
|
3042
|
+
function sortGithubGreptileReviews(reviews) {
|
|
3043
|
+
return reviews.filter((review) => isGreptileGithubLogin(review.user?.login)).sort((left, right) => Date.parse(right.submitted_at || "") - Date.parse(left.submitted_at || ""));
|
|
3044
|
+
}
|
|
3045
|
+
function loadGithubPullRequestCheckRollup(projectRoot, repoName, prNumber) {
|
|
3046
|
+
const response = runGhJson(projectRoot, [
|
|
3047
|
+
"pr",
|
|
3048
|
+
"view",
|
|
3049
|
+
`${prNumber}`,
|
|
3050
|
+
"--repo",
|
|
3051
|
+
repoName,
|
|
3052
|
+
"--json",
|
|
3053
|
+
"statusCheckRollup"
|
|
3054
|
+
]);
|
|
3055
|
+
return response.statusCheckRollup || [];
|
|
3056
|
+
}
|
|
3057
|
+
function evaluatePullRequestCiChecks(checks, repoName, prNumber) {
|
|
3058
|
+
const nonGreptileChecks = checks.filter((check) => {
|
|
3059
|
+
const label = (check.name || check.context || "").toLowerCase();
|
|
3060
|
+
return label.length > 0 && !label.includes("greptile");
|
|
3061
|
+
});
|
|
3062
|
+
const pending = nonGreptileChecks.filter((check) => {
|
|
3063
|
+
if ((check.__typename || "") === "CheckRun") {
|
|
3064
|
+
return (check.status || "").toUpperCase() !== "COMPLETED";
|
|
3065
|
+
}
|
|
3066
|
+
const state = (check.state || check.status || "").toUpperCase();
|
|
3067
|
+
return state === "PENDING" || state === "EXPECTED" || state === "QUEUED" || state === "IN_PROGRESS";
|
|
3068
|
+
});
|
|
3069
|
+
if (pending.length > 0) {
|
|
3070
|
+
return {
|
|
3071
|
+
verdict: "SKIP",
|
|
3072
|
+
reasons: pending.map((check) => `[CI] ${repoName}#${prNumber} check is still pending: ${check.name || check.context || "unknown"}.`)
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
const failing = nonGreptileChecks.filter((check) => {
|
|
3076
|
+
if ((check.__typename || "") === "CheckRun") {
|
|
3077
|
+
const conclusion = (check.conclusion || "").toUpperCase();
|
|
3078
|
+
return conclusion.length > 0 && !["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion);
|
|
3079
|
+
}
|
|
3080
|
+
const state = (check.state || check.conclusion || "").toUpperCase();
|
|
3081
|
+
return state.length > 0 && !["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state);
|
|
3082
|
+
});
|
|
3083
|
+
if (failing.length > 0) {
|
|
3084
|
+
return {
|
|
3085
|
+
verdict: "REJECT",
|
|
3086
|
+
reasons: failing.map((check) => `[CI] ${repoName}#${prNumber} check failed: ${check.name || check.context || "unknown"} (${check.conclusion || check.state || check.status || "unknown"}).`)
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
return { verdict: "APPROVE", reasons: [] };
|
|
3090
|
+
}
|
|
3091
|
+
function classifyGithubGreptileCheckState(checks) {
|
|
3092
|
+
const greptileChecks = checks.filter((check) => {
|
|
3093
|
+
const label = (check.name || check.context || "").toLowerCase();
|
|
3094
|
+
return label.includes("greptile");
|
|
3095
|
+
});
|
|
3096
|
+
if (greptileChecks.length === 0) {
|
|
3097
|
+
return { pending: false, completed: false };
|
|
3098
|
+
}
|
|
3099
|
+
for (const check of greptileChecks) {
|
|
3100
|
+
if ((check.__typename || "") === "CheckRun") {
|
|
3101
|
+
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
3102
|
+
return { pending: true, completed: false };
|
|
3103
|
+
}
|
|
3104
|
+
return { pending: false, completed: true };
|
|
3105
|
+
}
|
|
3106
|
+
const state = (check.state || "").toUpperCase();
|
|
3107
|
+
if (state === "PENDING" || state === "EXPECTED") {
|
|
3108
|
+
return { pending: true, completed: false };
|
|
3109
|
+
}
|
|
3110
|
+
if (state) {
|
|
3111
|
+
return { pending: false, completed: true };
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return { pending: false, completed: false };
|
|
3115
|
+
}
|
|
3116
|
+
function isGithubGreptileCheckApproved(checks) {
|
|
3117
|
+
const greptileChecks = checks.filter((check) => {
|
|
3118
|
+
const label = (check.name || check.context || "").toLowerCase();
|
|
3119
|
+
return label.includes("greptile");
|
|
3120
|
+
});
|
|
3121
|
+
if (greptileChecks.length === 0) {
|
|
3122
|
+
return false;
|
|
3123
|
+
}
|
|
3124
|
+
for (const check of greptileChecks) {
|
|
3125
|
+
if ((check.__typename || "") === "CheckRun") {
|
|
3126
|
+
if ((check.status || "").toUpperCase() !== "COMPLETED") {
|
|
3127
|
+
return false;
|
|
3128
|
+
}
|
|
3129
|
+
const conclusion = (check.conclusion || "").toUpperCase();
|
|
3130
|
+
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(conclusion)) {
|
|
3131
|
+
return false;
|
|
3132
|
+
}
|
|
3133
|
+
continue;
|
|
3134
|
+
}
|
|
3135
|
+
const state = (check.state || "").toUpperCase();
|
|
3136
|
+
if (!["SUCCESS", "NEUTRAL", "SKIPPED"].includes(state)) {
|
|
3137
|
+
return false;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
return true;
|
|
3141
|
+
}
|
|
3142
|
+
function loadGithubReviewThreads(projectRoot, repoName, prNumber) {
|
|
3143
|
+
const [owner, name] = repoName.split("/");
|
|
3144
|
+
if (!owner || !name) {
|
|
3145
|
+
return [];
|
|
3146
|
+
}
|
|
3147
|
+
const response = runGhJson(projectRoot, [
|
|
3148
|
+
"api",
|
|
3149
|
+
"graphql",
|
|
3150
|
+
"-F",
|
|
3151
|
+
`owner=${owner}`,
|
|
3152
|
+
"-F",
|
|
3153
|
+
`name=${name}`,
|
|
3154
|
+
"-F",
|
|
3155
|
+
`prNumber=${prNumber}`,
|
|
3156
|
+
"-f",
|
|
3157
|
+
"query=query($owner: String!, $name: String!, $prNumber: Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$prNumber) { reviewThreads(first: 100) { nodes { isResolved isOutdated comments(first: 20) { nodes { author { login } body path url createdAt } } } } } } }"
|
|
3158
|
+
]);
|
|
3159
|
+
return response.data?.repository?.pullRequest?.reviewThreads?.nodes || [];
|
|
3160
|
+
}
|
|
3161
|
+
function filterActionableGithubGreptileThreads(threads) {
|
|
3162
|
+
return threads.flatMap((thread) => {
|
|
3163
|
+
if (thread.isResolved || thread.isOutdated) {
|
|
3164
|
+
return [];
|
|
3165
|
+
}
|
|
3166
|
+
const comments = thread.comments?.nodes || [];
|
|
3167
|
+
const latestGreptileComment = [...comments].reverse().find((comment) => isGreptileGithubLogin(comment.author?.login));
|
|
3168
|
+
if (!latestGreptileComment?.path?.trim()) {
|
|
3169
|
+
return [];
|
|
3170
|
+
}
|
|
3171
|
+
return [latestGreptileComment];
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
function resolvePrRepoRoot(projectRoot, prState) {
|
|
3175
|
+
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
3176
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync11(resolve13(runtimeWorkspace, ".git"))) {
|
|
3177
|
+
return runtimeWorkspace;
|
|
3178
|
+
}
|
|
3179
|
+
const paths = resolveHarnessPaths(projectRoot);
|
|
3180
|
+
return prState.target === "monorepo" ? paths.monorepoRoot : projectRoot;
|
|
3181
|
+
}
|
|
3182
|
+
function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headCommit) {
|
|
3183
|
+
if (!reviewedCommit || !headCommit) {
|
|
3184
|
+
return false;
|
|
3185
|
+
}
|
|
3186
|
+
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
3187
|
+
return runCapture(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
3188
|
+
}
|
|
3189
|
+
function stripHtml(input) {
|
|
3190
|
+
return input.replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\r/g, "").replace(/\n{3,}/g, `
|
|
3191
|
+
|
|
3192
|
+
`).trim();
|
|
3193
|
+
}
|
|
3194
|
+
function summarizeComment(input) {
|
|
3195
|
+
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
3196
|
+
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
3197
|
+
}
|
|
3198
|
+
function asGreptileInfrastructureWarning(reason) {
|
|
3199
|
+
return reason.startsWith("[AI Review]") ? reason.replace("[AI Review]", "[AI Review Warning]") : reason;
|
|
3200
|
+
}
|
|
3201
|
+
function isAiReviewApproved(input) {
|
|
3202
|
+
if (input.reviewMode !== "required") {
|
|
3203
|
+
return true;
|
|
3204
|
+
}
|
|
3205
|
+
return input.aiVerdict === "APPROVE" && input.aiReasons.length === 0;
|
|
3206
|
+
}
|
|
3207
|
+
function parseGreptileScore(input) {
|
|
3208
|
+
const text = stripHtml(input);
|
|
3209
|
+
const patterns = [
|
|
3210
|
+
/confidence score:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
3211
|
+
/\bscore:\s*(\d+)\s*\/\s*(\d+)/i,
|
|
3212
|
+
/\b(\d+)\s*\/\s*(\d+)\s*(?:confidence|score)/i
|
|
3213
|
+
];
|
|
3214
|
+
for (const pattern of patterns) {
|
|
3215
|
+
const match = pattern.exec(text);
|
|
3216
|
+
if (!match) {
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
const value = Number.parseInt(match[1] || "", 10);
|
|
3220
|
+
const scale = Number.parseInt(match[2] || "", 10);
|
|
3221
|
+
if (Number.isFinite(value) && Number.isFinite(scale) && scale > 0) {
|
|
3222
|
+
return { value, scale };
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
var __testOnly = {
|
|
3228
|
+
asGreptileInfrastructureWarning,
|
|
3229
|
+
callGreptileMcpToolWithTimeout,
|
|
3230
|
+
classifyGithubGreptileCheckState,
|
|
3231
|
+
filterActionableGithubGreptileThreads,
|
|
3232
|
+
isGithubGreptileCheckApproved,
|
|
3233
|
+
isAiReviewApproved,
|
|
3234
|
+
parseGreptileScore,
|
|
3235
|
+
pickLatestGithubGreptileReview,
|
|
3236
|
+
pickRelevantGithubGreptileReview,
|
|
3237
|
+
resolveGreptileRequestTimeoutMs,
|
|
3238
|
+
resolveGreptilePollSettings,
|
|
3239
|
+
shouldPreferGithubGreptileFallback,
|
|
3240
|
+
shouldContinueGithubGreptileFallbackPolling,
|
|
3241
|
+
shouldContinueGreptileMcpPolling,
|
|
3242
|
+
shouldTriggerGreptileReview
|
|
3243
|
+
};
|
|
3244
|
+
export {
|
|
3245
|
+
verifyTask,
|
|
3246
|
+
pickRelevantCodeReview,
|
|
3247
|
+
filterActionableGreptileComments,
|
|
3248
|
+
__testOnly
|
|
3249
|
+
};
|