@h-rig/bundle-default-lifecycle 0.0.6-alpha.157 → 0.0.6-alpha.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli.d.ts +1 -7
- package/dist/src/cli.js +5 -2
- package/dist/src/control-plane/completion-verification.js +1591 -118
- package/dist/src/control-plane/hooks/inject-context.d.ts +2 -0
- package/dist/src/control-plane/hooks/inject-context.js +175 -0
- package/dist/src/control-plane/hooks/shared.d.ts +11 -0
- package/dist/src/control-plane/hooks/shared.js +44 -0
- package/dist/src/control-plane/hooks/submodule-branch.d.ts +2 -0
- package/dist/src/control-plane/hooks/submodule-branch.js +432 -0
- package/dist/src/control-plane/hooks/task-runtime-start.d.ts +2 -0
- package/dist/src/control-plane/hooks/task-runtime-start.js +429 -0
- package/dist/src/control-plane/materialize-task-config.d.ts +29 -0
- package/dist/src/control-plane/materialize-task-config.js +95 -0
- package/dist/src/control-plane/native/git-ops.d.ts +67 -0
- package/dist/src/control-plane/native/git-ops.js +1390 -0
- package/dist/src/control-plane/policy.d.ts +3 -0
- package/dist/src/control-plane/policy.js +226 -0
- package/dist/src/control-plane/pr-automation.d.ts +2 -0
- package/dist/src/control-plane/pr-automation.js +26 -16
- package/dist/src/control-plane/pr-merge-gate-cap.d.ts +10 -0
- package/dist/src/control-plane/pr-merge-gate-cap.js +13 -0
- package/dist/src/control-plane/task-data.d.ts +13 -0
- package/dist/src/control-plane/task-data.js +12 -0
- package/dist/src/control-plane/task-verify.js +131 -59
- package/dist/src/control-plane/verifier.d.ts +1 -3
- package/dist/src/control-plane/verifier.js +133 -57
- package/dist/src/defaultPipeline.d.ts +1 -1
- package/dist/src/defaultPipeline.js +5 -2
- package/dist/src/index.d.ts +0 -2
- package/dist/src/index.js +1908 -290
- package/dist/src/native/closeout-runners.js +22 -2
- package/dist/src/native/github-auth-env.d.ts +2 -0
- package/dist/src/native/github-auth-env.js +25 -0
- package/dist/src/native/host-git.d.ts +6 -0
- package/dist/src/native/host-git.js +62 -0
- package/dist/src/native/in-process-closeout.d.ts +1 -3
- package/dist/src/native/in-process-closeout.js +0 -794
- package/dist/src/pipelineCloseout.js +1905 -185
- package/dist/src/plugin.js +2843 -145
- package/dist/src/stages/auto-merge.js +28 -16
- package/dist/src/stages/commit.js +28 -16
- package/dist/src/stages/isolation.d.ts +1 -1
- package/dist/src/stages/isolation.js +5 -3
- package/dist/src/stages/merge-gate.js +35 -3
- package/dist/src/stages/open-pr.js +28 -16
- package/dist/src/stages/push.js +28 -16
- package/dist/src/stages/source-closeout.js +28 -16
- package/package.json +29 -16
- package/dist/src/branch-naming.d.ts +0 -15
- package/dist/src/branch-naming.js +0 -33
- package/dist/src/closeoutEquivalence.d.ts +0 -37
- package/dist/src/closeoutEquivalence.js +0 -78
- package/dist/src/closeoutShadowHarness.d.ts +0 -27
- package/dist/src/closeoutShadowHarness.js +0 -29
|
@@ -2,46 +2,1521 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
|
|
5
|
-
import { appendFileSync, existsSync as
|
|
6
|
-
import { resolve as
|
|
7
|
-
import { safePathSegment } from "@rig/
|
|
5
|
+
import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
6
|
+
import { resolve as resolve5 } from "path";
|
|
7
|
+
import { safePathSegment as safePathSegment2 } from "@rig/core/safe-identifiers";
|
|
8
8
|
import {
|
|
9
|
-
escapeRegExp,
|
|
9
|
+
escapeRegExp as escapeRegExp2,
|
|
10
10
|
resolveBunCli,
|
|
11
11
|
resolveBunCliInvocation,
|
|
12
12
|
resolveTaskScopes,
|
|
13
13
|
resolvePolicyContent
|
|
14
14
|
} from "@rig/hook-kit";
|
|
15
|
-
import { loadPolicy, seedPolicyFromContent } from "@rig/runtime/control-plane/runtime/guard";
|
|
16
|
-
import { gitCommit, gitMergePr, gitOpenPr, readPrMetadata as readPrMetadata2, resolveTaskBranchRef } from "@rig/runtime/control-plane/native/git-ops";
|
|
17
|
-
import { runStrictPrMergeGate } from "@rig/pr-review-plugin";
|
|
18
|
-
import { strictMergeHeadShaFromGate } from "@rig/contracts";
|
|
19
15
|
|
|
20
|
-
// packages/bundle-default-lifecycle/src/control-plane/
|
|
21
|
-
import { existsSync,
|
|
16
|
+
// packages/bundle-default-lifecycle/src/control-plane/policy.ts
|
|
17
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
22
18
|
import { resolve } from "path";
|
|
23
|
-
import { resolveRuntimeSecrets } from "@rig/runtime/control-plane/runtime/baked-secrets";
|
|
24
|
-
import { readPrMetadata } from "@rig/runtime/control-plane/native/git-ops";
|
|
25
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
26
|
-
import { readConfiguredTaskSourceTask } from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
27
|
-
import { artifactDirForId, lookupTask, readTaskConfig } from "@rig/runtime/control-plane/native/task-state";
|
|
28
|
-
import { nowIso, resolveHarnessPaths, runCapture } from "@rig/runtime/control-plane/native/utils";
|
|
29
19
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
POLICY_VERSION
|
|
21
|
+
} from "@rig/contracts";
|
|
22
|
+
var DEFAULT_SCOPE = {
|
|
23
|
+
fail_closed: true,
|
|
24
|
+
harness_paths_exempt: true,
|
|
25
|
+
runtime_paths_exempt: true
|
|
26
|
+
};
|
|
27
|
+
var DEFAULT_SANDBOX = {
|
|
28
|
+
mode: "enforce",
|
|
29
|
+
network: true,
|
|
30
|
+
read_deny: [],
|
|
31
|
+
write_allow_from_runtime: true
|
|
32
|
+
};
|
|
33
|
+
var DEFAULT_ISOLATION = {
|
|
34
|
+
default_mode: "worktree",
|
|
35
|
+
repo_symlink_fallback: false,
|
|
36
|
+
strict_provisioning: true,
|
|
37
|
+
fail_closed_on_provision_error: true
|
|
38
|
+
};
|
|
39
|
+
var DEFAULT_COMPLETION = {
|
|
40
|
+
derive_checks_from_scope: true,
|
|
41
|
+
checks: [],
|
|
42
|
+
typescript_config_probe: ["tsconfig.json"],
|
|
43
|
+
eslint_config_probe: [".eslintrc.js", ".eslintrc.json", "eslint.config.js"]
|
|
44
|
+
};
|
|
45
|
+
var DEFAULT_RUNTIME_IMAGE = {
|
|
46
|
+
deps: {
|
|
47
|
+
monorepo_install: false,
|
|
48
|
+
hp_next_install: false
|
|
49
|
+
},
|
|
50
|
+
plugins_require_binaries: true
|
|
51
|
+
};
|
|
52
|
+
var DEFAULT_RUNTIME_SNAPSHOT = {
|
|
53
|
+
enabled: true
|
|
54
|
+
};
|
|
55
|
+
var policyCache = null;
|
|
56
|
+
var policyCachePath = null;
|
|
57
|
+
var seededPolicyConfig = null;
|
|
58
|
+
function defaultPolicy() {
|
|
59
|
+
return {
|
|
60
|
+
version: POLICY_VERSION,
|
|
61
|
+
mode: "enforce",
|
|
62
|
+
scope: { ...DEFAULT_SCOPE },
|
|
63
|
+
rules: [],
|
|
64
|
+
sandbox: { ...DEFAULT_SANDBOX },
|
|
65
|
+
isolation: { ...DEFAULT_ISOLATION },
|
|
66
|
+
completion: { ...DEFAULT_COMPLETION },
|
|
67
|
+
runtime_image: {
|
|
68
|
+
deps: { ...DEFAULT_RUNTIME_IMAGE.deps },
|
|
69
|
+
plugins_require_binaries: DEFAULT_RUNTIME_IMAGE.plugins_require_binaries
|
|
70
|
+
},
|
|
71
|
+
runtime_snapshot: { ...DEFAULT_RUNTIME_SNAPSHOT }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function seedPolicyFromContent(rawJson) {
|
|
75
|
+
try {
|
|
76
|
+
seededPolicyConfig = mergeWithDefaults(JSON.parse(rawJson));
|
|
77
|
+
} catch {
|
|
78
|
+
seededPolicyConfig = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function loadPolicy(projectRoot) {
|
|
82
|
+
if (seededPolicyConfig) {
|
|
83
|
+
return seededPolicyConfig;
|
|
84
|
+
}
|
|
85
|
+
const configPath = resolve(projectRoot, "rig/policy/policy.json");
|
|
86
|
+
if (!existsSync(configPath)) {
|
|
87
|
+
return defaultPolicy();
|
|
88
|
+
}
|
|
89
|
+
let mtimeMs;
|
|
90
|
+
try {
|
|
91
|
+
mtimeMs = statSync(configPath).mtimeMs;
|
|
92
|
+
} catch {
|
|
93
|
+
return defaultPolicy();
|
|
94
|
+
}
|
|
95
|
+
if (policyCache && policyCachePath === configPath && policyCache.mtimeMs === mtimeMs) {
|
|
96
|
+
return policyCache.config;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const config = mergeWithDefaults(JSON.parse(readFileSync(configPath, "utf-8")));
|
|
100
|
+
policyCache = { mtimeMs, config };
|
|
101
|
+
policyCachePath = configPath;
|
|
102
|
+
return config;
|
|
103
|
+
} catch {
|
|
104
|
+
return defaultPolicy();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function mergeWithDefaults(parsed) {
|
|
108
|
+
const base = defaultPolicy();
|
|
109
|
+
if (typeof parsed.mode === "string" && isValidMode(parsed.mode)) {
|
|
110
|
+
base.mode = parsed.mode;
|
|
111
|
+
}
|
|
112
|
+
if (parsed.scope && typeof parsed.scope === "object" && !Array.isArray(parsed.scope)) {
|
|
113
|
+
const scope = parsed.scope;
|
|
114
|
+
if (typeof scope.fail_closed === "boolean")
|
|
115
|
+
base.scope.fail_closed = scope.fail_closed;
|
|
116
|
+
if (typeof scope.harness_paths_exempt === "boolean")
|
|
117
|
+
base.scope.harness_paths_exempt = scope.harness_paths_exempt;
|
|
118
|
+
if (typeof scope.runtime_paths_exempt === "boolean")
|
|
119
|
+
base.scope.runtime_paths_exempt = scope.runtime_paths_exempt;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(parsed.rules)) {
|
|
122
|
+
base.rules = precompilePolicyRuleRegexes(parsed.rules.filter(isValidRule));
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(parsed.deny) && base.rules.length === 0) {
|
|
125
|
+
base.rules = precompilePolicyRuleRegexes(migrateLegacyDeny(parsed.deny));
|
|
126
|
+
}
|
|
127
|
+
if (parsed.sandbox && typeof parsed.sandbox === "object" && !Array.isArray(parsed.sandbox)) {
|
|
128
|
+
const sandbox = parsed.sandbox;
|
|
129
|
+
if (typeof sandbox.mode === "string" && isValidMode(sandbox.mode))
|
|
130
|
+
base.sandbox.mode = sandbox.mode;
|
|
131
|
+
if (typeof sandbox.network === "boolean")
|
|
132
|
+
base.sandbox.network = sandbox.network;
|
|
133
|
+
if (Array.isArray(sandbox.read_deny))
|
|
134
|
+
base.sandbox.read_deny = sandbox.read_deny.filter((value) => typeof value === "string");
|
|
135
|
+
if (typeof sandbox.write_allow_from_runtime === "boolean")
|
|
136
|
+
base.sandbox.write_allow_from_runtime = sandbox.write_allow_from_runtime;
|
|
137
|
+
}
|
|
138
|
+
if (parsed.isolation && typeof parsed.isolation === "object" && !Array.isArray(parsed.isolation)) {
|
|
139
|
+
const isolation = parsed.isolation;
|
|
140
|
+
if (isolation.default_mode === "worktree")
|
|
141
|
+
base.isolation.default_mode = isolation.default_mode;
|
|
142
|
+
if (typeof isolation.repo_symlink_fallback === "boolean")
|
|
143
|
+
base.isolation.repo_symlink_fallback = isolation.repo_symlink_fallback;
|
|
144
|
+
if (typeof isolation.strict_provisioning === "boolean")
|
|
145
|
+
base.isolation.strict_provisioning = isolation.strict_provisioning;
|
|
146
|
+
if (typeof isolation.fail_closed_on_provision_error === "boolean")
|
|
147
|
+
base.isolation.fail_closed_on_provision_error = isolation.fail_closed_on_provision_error;
|
|
148
|
+
}
|
|
149
|
+
if (parsed.completion && typeof parsed.completion === "object" && !Array.isArray(parsed.completion)) {
|
|
150
|
+
const completion = parsed.completion;
|
|
151
|
+
if (typeof completion.derive_checks_from_scope === "boolean")
|
|
152
|
+
base.completion.derive_checks_from_scope = completion.derive_checks_from_scope;
|
|
153
|
+
if (Array.isArray(completion.checks))
|
|
154
|
+
base.completion.checks = completion.checks.filter((value) => typeof value === "string");
|
|
155
|
+
if (Array.isArray(completion.typescript_config_probe))
|
|
156
|
+
base.completion.typescript_config_probe = completion.typescript_config_probe.filter((value) => typeof value === "string");
|
|
157
|
+
if (Array.isArray(completion.eslint_config_probe))
|
|
158
|
+
base.completion.eslint_config_probe = completion.eslint_config_probe.filter((value) => typeof value === "string");
|
|
159
|
+
}
|
|
160
|
+
if (parsed.runtime_image && typeof parsed.runtime_image === "object" && !Array.isArray(parsed.runtime_image)) {
|
|
161
|
+
const runtimeImage = parsed.runtime_image;
|
|
162
|
+
if (runtimeImage.deps && typeof runtimeImage.deps === "object" && !Array.isArray(runtimeImage.deps)) {
|
|
163
|
+
const deps = runtimeImage.deps;
|
|
164
|
+
if (typeof deps.monorepo_install === "boolean")
|
|
165
|
+
base.runtime_image.deps.monorepo_install = deps.monorepo_install;
|
|
166
|
+
if (typeof deps.hp_next_install === "boolean")
|
|
167
|
+
base.runtime_image.deps.hp_next_install = deps.hp_next_install;
|
|
168
|
+
}
|
|
169
|
+
if (typeof runtimeImage.plugins_require_binaries === "boolean") {
|
|
170
|
+
base.runtime_image.plugins_require_binaries = runtimeImage.plugins_require_binaries;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (parsed.runtime_snapshot && typeof parsed.runtime_snapshot === "object" && !Array.isArray(parsed.runtime_snapshot)) {
|
|
174
|
+
const runtimeSnapshot = parsed.runtime_snapshot;
|
|
175
|
+
if (typeof runtimeSnapshot.enabled === "boolean")
|
|
176
|
+
base.runtime_snapshot.enabled = runtimeSnapshot.enabled;
|
|
177
|
+
}
|
|
178
|
+
return base;
|
|
179
|
+
}
|
|
180
|
+
function isValidMode(value) {
|
|
181
|
+
return value === "off" || value === "observe" || value === "enforce";
|
|
182
|
+
}
|
|
183
|
+
function isValidRule(value) {
|
|
184
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
185
|
+
return false;
|
|
186
|
+
const rule = value;
|
|
187
|
+
return typeof rule.id === "string" && typeof rule.category === "string" && !!rule.match && typeof rule.match === "object";
|
|
188
|
+
}
|
|
189
|
+
function migrateLegacyDeny(deny) {
|
|
190
|
+
const rules = [];
|
|
191
|
+
for (const entry of deny) {
|
|
192
|
+
if (typeof entry.id !== "string")
|
|
193
|
+
continue;
|
|
194
|
+
const match = {};
|
|
195
|
+
if (typeof entry.pattern === "string")
|
|
196
|
+
match.pattern = entry.pattern;
|
|
197
|
+
if (typeof entry.regex === "string")
|
|
198
|
+
match.regex = entry.regex;
|
|
199
|
+
if (!match.pattern && !match.regex)
|
|
200
|
+
continue;
|
|
201
|
+
const rule = {
|
|
202
|
+
id: entry.id,
|
|
203
|
+
category: "command",
|
|
204
|
+
match,
|
|
205
|
+
action: entry.action === "warn" ? "warn" : "block"
|
|
206
|
+
};
|
|
207
|
+
if (typeof entry.reason === "string") {
|
|
208
|
+
rule.description = entry.reason;
|
|
209
|
+
}
|
|
210
|
+
rules.push(rule);
|
|
211
|
+
}
|
|
212
|
+
return rules;
|
|
213
|
+
}
|
|
214
|
+
function precompilePolicyRuleRegexes(rules) {
|
|
215
|
+
return rules.map((rule) => {
|
|
216
|
+
const compiled = { ...rule };
|
|
217
|
+
const matchRegex = compileRegex(rule.match?.regex);
|
|
218
|
+
const unlessRegex = compileRegex(rule.unless?.regex);
|
|
219
|
+
if (matchRegex) {
|
|
220
|
+
compiled.compiledRegex = matchRegex;
|
|
221
|
+
}
|
|
222
|
+
if (unlessRegex) {
|
|
223
|
+
compiled.compiledUnlessRegex = unlessRegex;
|
|
224
|
+
}
|
|
225
|
+
return compiled;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
function compileRegex(pattern) {
|
|
229
|
+
if (!pattern)
|
|
230
|
+
return;
|
|
231
|
+
try {
|
|
232
|
+
return new RegExp(pattern);
|
|
233
|
+
} catch {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
|
|
239
|
+
import { existsSync as existsSync4, lstatSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
|
|
240
|
+
import { tmpdir } from "os";
|
|
241
|
+
import { dirname, isAbsolute, resolve as resolve3 } from "path";
|
|
242
|
+
import { fileURLToPath } from "url";
|
|
243
|
+
import { loadDotEnvSecrets, resolveRuntimeSecrets } from "@rig/core/baked-secrets";
|
|
244
|
+
import { loadRuntimeContext, loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
|
|
245
|
+
|
|
246
|
+
// packages/bundle-default-lifecycle/src/control-plane/task-data.ts
|
|
247
|
+
import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
|
|
248
|
+
import { defineCapability } from "@rig/core/capability";
|
|
249
|
+
import { requireInstalledCapability } from "@rig/core/capability-loaders";
|
|
250
|
+
var TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
|
|
251
|
+
function taskData() {
|
|
252
|
+
return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running the lifecycle.");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
|
|
256
|
+
import { nowIso, runCapture as baseRunCapture } from "@rig/core/exec";
|
|
257
|
+
import { resolveCheckoutRoot as resolveMonorepoRoot } from "@rig/core/checkout-root";
|
|
258
|
+
import { getScopeRules } from "@rig/core/scope-rules";
|
|
259
|
+
|
|
260
|
+
// packages/bundle-default-lifecycle/src/native/github-auth-env.ts
|
|
261
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
262
|
+
function cleanToken(value) {
|
|
263
|
+
const trimmed = value?.trim() ?? "";
|
|
264
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
265
|
+
}
|
|
266
|
+
function authStateToken(env = process.env) {
|
|
267
|
+
const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
|
|
268
|
+
if (!file || !existsSync2(file))
|
|
269
|
+
return null;
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(readFileSync2(file, "utf8"));
|
|
272
|
+
return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
|
|
273
|
+
} catch {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
|
|
279
|
+
import { safePathSegment } from "@rig/core/safe-identifiers";
|
|
280
|
+
|
|
281
|
+
// packages/bundle-default-lifecycle/src/native/host-git.ts
|
|
282
|
+
import { existsSync as existsSync3 } from "fs";
|
|
283
|
+
import { resolve as resolve2 } from "path";
|
|
284
|
+
function isRuntimeGatewayGitPath(candidate) {
|
|
285
|
+
return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
|
|
286
|
+
}
|
|
287
|
+
function isRuntimeGatewayGhPath(candidate) {
|
|
288
|
+
return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
|
|
289
|
+
}
|
|
290
|
+
function resolveHostGitBinary() {
|
|
291
|
+
const candidates = [
|
|
292
|
+
process.env.RIG_GIT_BIN?.trim() || "",
|
|
293
|
+
"/usr/bin/git",
|
|
294
|
+
"/opt/homebrew/bin/git",
|
|
295
|
+
"/usr/local/bin/git"
|
|
296
|
+
];
|
|
297
|
+
const bunResolved = Bun.which("git");
|
|
298
|
+
if (bunResolved && !isRuntimeGatewayGitPath(bunResolved)) {
|
|
299
|
+
candidates.push(bunResolved);
|
|
300
|
+
}
|
|
301
|
+
for (const candidate of candidates) {
|
|
302
|
+
if (!candidate || isRuntimeGatewayGitPath(candidate)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (existsSync3(candidate)) {
|
|
306
|
+
return candidate;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return "git";
|
|
310
|
+
}
|
|
311
|
+
function resolveGithubCliBinary(options = {}) {
|
|
312
|
+
const candidates = new Set;
|
|
313
|
+
const explicit = process.env.RIG_GH_BIN?.trim();
|
|
314
|
+
if (explicit) {
|
|
315
|
+
candidates.add(explicit);
|
|
316
|
+
}
|
|
317
|
+
for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"]) {
|
|
318
|
+
candidates.add(candidate);
|
|
319
|
+
}
|
|
320
|
+
if (options.scanPath) {
|
|
321
|
+
for (const entry of (process.env.PATH || "").split(":").map((e) => e.trim()).filter(Boolean)) {
|
|
322
|
+
candidates.add(resolve2(entry, "gh"));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const bunResolved = Bun.which("gh");
|
|
326
|
+
if (bunResolved) {
|
|
327
|
+
candidates.add(bunResolved);
|
|
328
|
+
}
|
|
329
|
+
for (const candidate of candidates) {
|
|
330
|
+
if (candidate && existsSync3(candidate) && !isRuntimeGatewayGhPath(candidate)) {
|
|
331
|
+
return candidate;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return "";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// packages/bundle-default-lifecycle/src/control-plane/native/git-ops.ts
|
|
338
|
+
var TASK_RUNTIME_STAGE_EXCLUDES = [
|
|
339
|
+
".rig/bin/**",
|
|
340
|
+
".rig/cache/**",
|
|
341
|
+
".rig/home/**",
|
|
342
|
+
".rig/logs/**",
|
|
343
|
+
".rig/runtime/**",
|
|
344
|
+
".rig/session/**",
|
|
345
|
+
".rig/state/**",
|
|
346
|
+
".rig/runtime-context.json"
|
|
347
|
+
];
|
|
348
|
+
var GENERATED_STAGE_EXCLUDES = ["artifacts/*/runtime-snapshots/**"];
|
|
349
|
+
var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
|
|
350
|
+
"changed-files.txt",
|
|
351
|
+
"contract-changes.md",
|
|
352
|
+
"decision-log.md",
|
|
353
|
+
"git-state.txt",
|
|
354
|
+
"next-actions.md",
|
|
355
|
+
"pr-state.json",
|
|
356
|
+
"task-result.json",
|
|
357
|
+
"validation-summary.json"
|
|
358
|
+
]);
|
|
359
|
+
function resolveOptionalMonorepoRoot(projectRoot) {
|
|
360
|
+
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
361
|
+
if (runtimeWorkspace && existsSync4(resolve3(runtimeWorkspace, ".git"))) {
|
|
362
|
+
return resolve3(runtimeWorkspace);
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
return resolveMonorepoRoot(projectRoot);
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function escapeRegExp(value) {
|
|
371
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
372
|
+
}
|
|
373
|
+
function safeCurrentTaskId(projectRoot) {
|
|
374
|
+
try {
|
|
375
|
+
const taskId = taskData().currentTaskId(projectRoot);
|
|
376
|
+
return /^bd-[a-z0-9-]+$/.test(taskId) ? taskId : "";
|
|
377
|
+
} catch {
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function gitCmd(projectRoot, repoRoot, ...args) {
|
|
382
|
+
return [resolveHostGitBinary(), "-C", repoRoot, ...args];
|
|
383
|
+
}
|
|
384
|
+
function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
|
|
385
|
+
const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
|
|
386
|
+
if (!resolvedTask) {
|
|
387
|
+
throw new Error("No task specified and no active task in session.");
|
|
388
|
+
}
|
|
389
|
+
const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
|
|
390
|
+
const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
|
|
391
|
+
if (!existsSync4(resolve3(repoRoot, ".git"))) {
|
|
392
|
+
throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
|
|
393
|
+
}
|
|
394
|
+
const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
|
|
395
|
+
const branchTarget = `rig/${branchId}`;
|
|
396
|
+
const current = branchName(projectRoot, repoRoot);
|
|
397
|
+
if (current === branchTarget) {
|
|
398
|
+
console.log(`${repoLabel} branch: already on ${branchTarget}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const hasBranch = runCapture(gitCmd(projectRoot, repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchTarget}`), projectRoot).exitCode === 0;
|
|
402
|
+
const cmd = hasBranch && current === "HEAD" ? gitCmd(projectRoot, repoRoot, "checkout", "-B", branchTarget) : hasBranch ? gitCmd(projectRoot, repoRoot, "checkout", branchTarget) : gitCmd(projectRoot, repoRoot, "checkout", "-b", branchTarget);
|
|
403
|
+
const checkout = runCapture(cmd, projectRoot);
|
|
404
|
+
if (checkout.exitCode !== 0) {
|
|
405
|
+
throw new Error(`Failed to sync ${repoLabel.toLowerCase()} branch: ${checkout.stderr || checkout.stdout}`);
|
|
406
|
+
}
|
|
407
|
+
const action = hasBranch && current === "HEAD" ? "reset" : hasBranch ? "checked out" : "created";
|
|
408
|
+
console.log(`${repoLabel} branch: ${action} ${branchTarget}`);
|
|
409
|
+
}
|
|
410
|
+
function gitCommit(options) {
|
|
411
|
+
const { projectRoot } = options;
|
|
412
|
+
const resolvedTask = options.taskId || safeCurrentTaskId(projectRoot);
|
|
413
|
+
const baseMessage = options.message || (resolvedTask ? `rig: ${resolvedTask}` : "rig: harness update");
|
|
414
|
+
const changedFilesManifest = resolvedTask && options.scoped === true ? refreshChangedFilesManifest(projectRoot, resolvedTask) : "";
|
|
415
|
+
const changedFiles = resolvedTask && options.scoped === true ? readChangedFilesManifest(projectRoot, resolvedTask) : resolvedTask ? taskData().changedFilesForTask(projectRoot, resolvedTask, false) : [];
|
|
416
|
+
if (options.target === "project" || options.target === "both") {
|
|
417
|
+
if (resolvedTask) {
|
|
418
|
+
gitSyncBranch(projectRoot, resolvedTask, "project");
|
|
419
|
+
}
|
|
420
|
+
const projectFiles = resolveScopedStageFilesForRepo(projectRoot, projectRoot, resolvedTask, changedFiles);
|
|
421
|
+
commitRepo(projectRoot, projectRoot, "project-rig", options.target === "both" ? `${baseMessage} [harness]` : baseMessage, options.allowEmpty, options.scoped === true, projectFiles, changedFilesManifest);
|
|
422
|
+
}
|
|
423
|
+
if (options.target === "monorepo" || options.target === "both") {
|
|
424
|
+
const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot);
|
|
425
|
+
if (resolvedTask) {
|
|
426
|
+
gitSyncBranch(projectRoot, resolvedTask, "monorepo");
|
|
427
|
+
}
|
|
428
|
+
const monorepoFiles = resolveScopedStageFilesForRepo(projectRoot, monorepoRoot, resolvedTask, changedFiles);
|
|
429
|
+
commitRepo(projectRoot, monorepoRoot, "monorepo", options.target === "both" ? `${baseMessage} [monorepo]` : baseMessage, options.allowEmpty, options.scoped === true, monorepoFiles, changedFilesManifest);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function gitOpenPr(options) {
|
|
433
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
434
|
+
if (!gh) {
|
|
435
|
+
throw new Error("gh CLI is required for open-pr. Install and authenticate with: gh auth login");
|
|
436
|
+
}
|
|
437
|
+
const taskId = options.taskId || safeCurrentTaskId(options.projectRoot);
|
|
438
|
+
const target = options.target || (taskId ? "monorepo" : "project");
|
|
439
|
+
let repoRoot = options.projectRoot;
|
|
440
|
+
let repoLabel = "project-rig";
|
|
441
|
+
const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
|
|
442
|
+
if (target === "monorepo") {
|
|
443
|
+
repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot(options.projectRoot);
|
|
444
|
+
repoLabel = "monorepo";
|
|
445
|
+
if (taskId) {
|
|
446
|
+
gitSyncBranch(options.projectRoot, taskId, "monorepo");
|
|
447
|
+
}
|
|
448
|
+
} else if (taskId) {
|
|
449
|
+
gitSyncBranch(options.projectRoot, taskId, "project");
|
|
450
|
+
}
|
|
451
|
+
if (!existsSync4(resolve3(repoRoot, ".git"))) {
|
|
452
|
+
throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
|
|
453
|
+
}
|
|
454
|
+
const branch = branchName(options.projectRoot, repoRoot);
|
|
455
|
+
if (!branch || branch === "HEAD") {
|
|
456
|
+
throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
|
|
457
|
+
}
|
|
458
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
459
|
+
const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
|
|
460
|
+
const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
|
|
461
|
+
refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
|
|
462
|
+
let reviewer = (options.reviewer || "").trim();
|
|
463
|
+
let reviewerSource = reviewer ? "flag" : undefined;
|
|
464
|
+
if (!reviewer && taskId) {
|
|
465
|
+
reviewer = defaultReviewerForTask(options.projectRoot, taskId);
|
|
466
|
+
if (reviewer) {
|
|
467
|
+
reviewerSource = "task-config";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!reviewer) {
|
|
471
|
+
reviewer = inferReviewerFromChangedFiles(options.projectRoot, repoRoot, base, branch);
|
|
472
|
+
if (reviewer) {
|
|
473
|
+
reviewerSource = "changed-files";
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!reviewer) {
|
|
477
|
+
reviewer = (process.env.RIG_PR_REVIEWER || "").trim();
|
|
478
|
+
if (reviewer) {
|
|
479
|
+
reviewerSource = "env";
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
let title = options.title || "";
|
|
483
|
+
if (!title) {
|
|
484
|
+
if (taskId) {
|
|
485
|
+
title = `rig: ${taskId}`;
|
|
486
|
+
} else {
|
|
487
|
+
title = `rig: update ${branch}`;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const body = options.body || [
|
|
491
|
+
"## Summary",
|
|
492
|
+
"- Automated task output prepared in isolated runtime.",
|
|
493
|
+
"",
|
|
494
|
+
"## Task",
|
|
495
|
+
`- beads: ${taskId || "n/a"}`,
|
|
496
|
+
...defaultPrRunLines(taskId, repoNameWithOwner),
|
|
497
|
+
"",
|
|
498
|
+
"## Review",
|
|
499
|
+
"- Completion verification will run validation, verifier review, and PR policy checks.",
|
|
500
|
+
"- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
|
|
501
|
+
].join(`
|
|
502
|
+
`);
|
|
503
|
+
const preCheck = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
504
|
+
const preCheckEntry = preCheck.exitCode === 0 ? preCheck.stdout.trim() : "";
|
|
505
|
+
if (preCheckEntry && preCheckEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
|
|
506
|
+
const mergedPr = JSON.parse(preCheckEntry);
|
|
507
|
+
console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
|
|
508
|
+
const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
|
|
509
|
+
if (taskId)
|
|
510
|
+
writePrMetadata(options.projectRoot, taskId, result2);
|
|
511
|
+
return result2;
|
|
512
|
+
}
|
|
513
|
+
const pushArgs = gitCmd(options.projectRoot, repoRoot, "push", "-u", networkRemote, branch);
|
|
514
|
+
const fetchResult = runCapture(gitCmd(options.projectRoot, repoRoot, "fetch", networkRemote, branch), repoRoot);
|
|
515
|
+
if (fetchResult.exitCode === 0) {
|
|
516
|
+
const remoteAhead = runCapture(gitCmd(options.projectRoot, repoRoot, "log", "--oneline", `HEAD..${networkRemote}/${branch}`), repoRoot);
|
|
517
|
+
if (remoteAhead.exitCode === 0 && remoteAhead.stdout.trim()) {
|
|
518
|
+
console.log(`Remote branch has diverged \u2014 force pushing task-owned branch ${branch} with --force-with-lease...`);
|
|
519
|
+
pushArgs.splice(4, 0, "--force-with-lease");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
runOrThrow(options.projectRoot, pushArgs, `Failed to push branch ${branch} in ${repoLabel}`);
|
|
523
|
+
const existing = runCapture(withGhRepo([gh, "pr", "list", "--state", "open", "--head", branch, "--json", "url", "--jq", ".[0].url"], repoNameWithOwner), repoRoot);
|
|
524
|
+
const existingUrl = existing.exitCode === 0 ? existing.stdout.trim() : "";
|
|
525
|
+
if (!existingUrl || existingUrl === "null") {
|
|
526
|
+
const merged = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
|
|
527
|
+
const mergedEntry = merged.exitCode === 0 ? merged.stdout.trim() : "";
|
|
528
|
+
if (mergedEntry && mergedEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
|
|
529
|
+
const mergedPr = JSON.parse(mergedEntry);
|
|
530
|
+
console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
|
|
531
|
+
const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
|
|
532
|
+
if (taskId)
|
|
533
|
+
writePrMetadata(options.projectRoot, taskId, result2);
|
|
534
|
+
return result2;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let prUrl = "";
|
|
538
|
+
if (existingUrl && existingUrl !== "null") {
|
|
539
|
+
prUrl = existingUrl;
|
|
540
|
+
} else {
|
|
541
|
+
const createArgs = [
|
|
542
|
+
gh,
|
|
543
|
+
"pr",
|
|
544
|
+
"create",
|
|
545
|
+
...ghRepoArgs(repoNameWithOwner),
|
|
546
|
+
"--base",
|
|
547
|
+
base,
|
|
548
|
+
"--head",
|
|
549
|
+
branch,
|
|
550
|
+
"--title",
|
|
551
|
+
title,
|
|
552
|
+
"--body",
|
|
553
|
+
body
|
|
554
|
+
];
|
|
555
|
+
if (options.draft) {
|
|
556
|
+
createArgs.push("--draft");
|
|
557
|
+
}
|
|
558
|
+
const created = runCapture(createArgs, repoRoot);
|
|
559
|
+
if (created.exitCode !== 0) {
|
|
560
|
+
throw new Error(`Failed to create PR in ${repoLabel}: ${created.stderr || created.stdout}`);
|
|
561
|
+
}
|
|
562
|
+
prUrl = created.stdout.trim();
|
|
563
|
+
}
|
|
564
|
+
if (!prUrl) {
|
|
565
|
+
throw new Error(`Failed to resolve PR URL for branch ${branch}.`);
|
|
566
|
+
}
|
|
567
|
+
assertPrHasNoGitConflicts(readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl), repoLabel, base);
|
|
568
|
+
if (reviewer) {
|
|
569
|
+
const edit = runCapture(withGhRepo([gh, "pr", "edit", prUrl, "--add-reviewer", reviewer], repoNameWithOwner), repoRoot);
|
|
570
|
+
if (edit.exitCode !== 0) {
|
|
571
|
+
throw new Error(`Failed to assign reviewer '${reviewer}': ${edit.stderr || edit.stdout}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
const result = {
|
|
575
|
+
url: prUrl,
|
|
576
|
+
...reviewer ? { reviewer } : {},
|
|
577
|
+
...reviewerSource ? { reviewerSource } : {},
|
|
578
|
+
target,
|
|
579
|
+
repoLabel,
|
|
580
|
+
branch,
|
|
581
|
+
base
|
|
582
|
+
};
|
|
583
|
+
if (taskId) {
|
|
584
|
+
writePrMetadata(options.projectRoot, taskId, result);
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
}
|
|
588
|
+
function defaultPrRunLines(taskId, repoNameWithOwner) {
|
|
589
|
+
const lines = [];
|
|
590
|
+
const runId = process.env.RIG_SERVER_RUN_ID?.trim();
|
|
591
|
+
if (runId) {
|
|
592
|
+
lines.push(`- Run: ${runId}`);
|
|
593
|
+
}
|
|
594
|
+
const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
|
|
595
|
+
if (closeout) {
|
|
596
|
+
lines.push(`- ${closeout}`);
|
|
597
|
+
}
|
|
598
|
+
return lines;
|
|
599
|
+
}
|
|
600
|
+
function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
|
|
601
|
+
const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
|
|
602
|
+
if (sourceIssueId) {
|
|
603
|
+
const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
|
|
604
|
+
if (match?.[1] && match[2]) {
|
|
605
|
+
const sourceRepo = match[1];
|
|
606
|
+
const issueNumber = match[2];
|
|
607
|
+
return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
|
|
611
|
+
}
|
|
612
|
+
function resolveTaskBranchRef(projectRoot, taskId) {
|
|
613
|
+
return `rig/${resolveTaskBranchId(projectRoot, taskId)}`;
|
|
614
|
+
}
|
|
615
|
+
function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
|
|
616
|
+
const view = runCapture(withGhRepo([
|
|
617
|
+
gh,
|
|
618
|
+
"pr",
|
|
619
|
+
"view",
|
|
620
|
+
prUrl,
|
|
621
|
+
"--json",
|
|
622
|
+
"state,isDraft,url,mergedAt,autoMergeRequest,mergeable,mergeStateStatus,reviewDecision,headRefOid,statusCheckRollup",
|
|
623
|
+
"--jq",
|
|
624
|
+
"."
|
|
625
|
+
], repoNameWithOwner), repoRoot);
|
|
626
|
+
if (view.exitCode !== 0) {
|
|
627
|
+
throw new Error(`Failed to inspect PR ${prUrl}: ${view.stderr || view.stdout}`);
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
const parsed = JSON.parse(view.stdout);
|
|
631
|
+
return {
|
|
632
|
+
state: parsed.state || "OPEN",
|
|
633
|
+
isDraft: parsed.isDraft === true,
|
|
634
|
+
url: typeof parsed.url === "string" ? parsed.url : prUrl,
|
|
635
|
+
mergedAt: typeof parsed.mergedAt === "string" ? parsed.mergedAt : null,
|
|
636
|
+
autoMergeRequest: parsed.autoMergeRequest ?? null,
|
|
637
|
+
mergeable: typeof parsed.mergeable === "string" ? parsed.mergeable : "",
|
|
638
|
+
mergeStateStatus: typeof parsed.mergeStateStatus === "string" ? parsed.mergeStateStatus : "",
|
|
639
|
+
reviewDecision: typeof parsed.reviewDecision === "string" ? parsed.reviewDecision : "",
|
|
640
|
+
headRefOid: typeof parsed.headRefOid === "string" ? parsed.headRefOid : null,
|
|
641
|
+
statusCheckRollup: Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
|
|
642
|
+
};
|
|
643
|
+
} catch {
|
|
644
|
+
return {
|
|
645
|
+
state: "OPEN",
|
|
646
|
+
isDraft: false,
|
|
647
|
+
url: prUrl,
|
|
648
|
+
mergedAt: null,
|
|
649
|
+
autoMergeRequest: null,
|
|
650
|
+
mergeable: "",
|
|
651
|
+
mergeStateStatus: "",
|
|
652
|
+
reviewDecision: "",
|
|
653
|
+
headRefOid: null,
|
|
654
|
+
statusCheckRollup: []
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function hasSatisfiedStatusChecks(prState) {
|
|
659
|
+
if (prState.statusCheckRollup.length === 0) {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
return prState.statusCheckRollup.every((entry) => {
|
|
663
|
+
if (entry.__typename === "CheckRun") {
|
|
664
|
+
const status = entry.status?.toUpperCase() || "";
|
|
665
|
+
const conclusion = entry.conclusion?.toUpperCase() || "";
|
|
666
|
+
return status === "COMPLETED" && (conclusion === "SUCCESS" || conclusion === "SKIPPED" || conclusion === "NEUTRAL");
|
|
667
|
+
}
|
|
668
|
+
if (entry.__typename === "StatusContext") {
|
|
669
|
+
return (entry.state?.toUpperCase() || "") === "SUCCESS";
|
|
670
|
+
}
|
|
671
|
+
return false;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function canAdminMergeApprovedPr(prState) {
|
|
675
|
+
return prState.state === "OPEN" && prState.autoMergeRequest !== null && prState.mergeable.toUpperCase() === "MERGEABLE" && prState.reviewDecision.toUpperCase() === "APPROVED" && hasSatisfiedStatusChecks(prState);
|
|
676
|
+
}
|
|
677
|
+
function gitMergePr(options) {
|
|
678
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
679
|
+
if (!gh) {
|
|
680
|
+
throw new Error("gh CLI is required for merge-pr. Install and authenticate with: gh auth login");
|
|
681
|
+
}
|
|
682
|
+
const repoRoot = resolveRepoRoot(options.projectRoot, options.pr.target);
|
|
683
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
|
|
684
|
+
if (!existsSync4(resolve3(repoRoot, ".git"))) {
|
|
685
|
+
throw new Error(`Repository not available for merge-pr target ${options.pr.target}: ${repoRoot}`);
|
|
686
|
+
}
|
|
687
|
+
const prState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
688
|
+
const state = prState.state;
|
|
689
|
+
const isDraft = prState.isDraft;
|
|
690
|
+
assertPrHasNoGitConflicts(prState, options.pr.repoLabel, options.pr.base);
|
|
691
|
+
if (state === "MERGED") {
|
|
692
|
+
console.log(`PR already merged (${options.pr.repoLabel}): ${options.pr.url}`);
|
|
693
|
+
return { status: "already-merged", url: options.pr.url };
|
|
694
|
+
}
|
|
695
|
+
if (state !== "OPEN") {
|
|
696
|
+
throw new Error(`Cannot merge PR ${options.pr.url}: state is ${state}.`);
|
|
697
|
+
}
|
|
698
|
+
if (isDraft) {
|
|
699
|
+
throw new Error(`Cannot merge draft PR ${options.pr.url}.`);
|
|
700
|
+
}
|
|
701
|
+
const mergeArgs = withGhRepo([gh, "pr", "merge", options.pr.url], repoNameWithOwner);
|
|
702
|
+
const method = options.method || "squash";
|
|
703
|
+
mergeArgs.push(method === "merge" ? "--merge" : method === "rebase" ? "--rebase" : "--squash");
|
|
704
|
+
mergeArgs.push("--match-head-commit", options.matchHeadCommit);
|
|
705
|
+
if (options.deleteBranch !== false) {
|
|
706
|
+
mergeArgs.push("--delete-branch");
|
|
707
|
+
}
|
|
708
|
+
const directMerge = runCapture(mergeArgs, repoRoot);
|
|
709
|
+
if (directMerge.exitCode === 0) {
|
|
710
|
+
console.log(`Merged PR (${options.pr.repoLabel}): ${options.pr.url}`);
|
|
711
|
+
return { status: "merged", url: options.pr.url };
|
|
712
|
+
}
|
|
713
|
+
const postDirectState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
714
|
+
if (canAdminMergeApprovedPr(postDirectState)) {
|
|
715
|
+
const adminMergeArgs = [...mergeArgs, "--admin"];
|
|
716
|
+
const adminMerge = runCapture(adminMergeArgs, repoRoot);
|
|
717
|
+
if (adminMerge.exitCode === 0) {
|
|
718
|
+
const postAdminMergeState = readPrViewState(gh, repoRoot, repoNameWithOwner, options.pr.url);
|
|
719
|
+
if (postAdminMergeState.state === "MERGED" || postAdminMergeState.mergedAt) {
|
|
720
|
+
console.log(`Merged PR (${options.pr.repoLabel}) with admin fallback: ${options.pr.url}`);
|
|
721
|
+
return { status: "merged", url: options.pr.url };
|
|
722
|
+
}
|
|
723
|
+
throw new Error(`Admin merge command succeeded for PR ${options.pr.url} in ${options.pr.repoLabel}, but GitHub still reports it open.`);
|
|
724
|
+
}
|
|
725
|
+
const adminMergeMessage = `${adminMerge.stderr}
|
|
726
|
+
${adminMerge.stdout}`.trim();
|
|
727
|
+
if (!/admin|administrator|permission|not permitted|not allowed/i.test(adminMergeMessage)) {
|
|
728
|
+
throw new Error(`Failed to admin-merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${adminMergeMessage}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const directMergeMessage = `${directMerge.stderr}
|
|
732
|
+
${directMerge.stdout}`.trim();
|
|
733
|
+
throw new Error(`Failed to merge PR ${options.pr.url} in ${options.pr.repoLabel}: ${directMergeMessage}`);
|
|
734
|
+
}
|
|
735
|
+
function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
|
|
736
|
+
const mergeable = prState.mergeable.toUpperCase();
|
|
737
|
+
const mergeStateStatus = prState.mergeStateStatus.toUpperCase();
|
|
738
|
+
if (mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY") {
|
|
739
|
+
throw new Error(`PR ${prState.url || "unknown"} conflicts with ${baseRef} in ${repoLabel} (mergeable=${prState.mergeable || "unknown"}, mergeStateStatus=${prState.mergeStateStatus || "unknown"}). Rebase or merge ${baseRef} and resolve conflicts before completion-verification.`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function writePrMetadata(projectRoot, taskId, result) {
|
|
743
|
+
const dir = taskData().artifactDirForId(projectRoot, taskId);
|
|
744
|
+
mkdirSync(dir, { recursive: true });
|
|
745
|
+
const path = resolve3(dir, "pr-state.json");
|
|
746
|
+
let prs = {};
|
|
747
|
+
if (existsSync4(path)) {
|
|
748
|
+
try {
|
|
749
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
750
|
+
if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
|
|
751
|
+
prs = parsed.prs;
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
prs = {};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
prs[result.target] = result;
|
|
758
|
+
const primary = prs.monorepo || prs.project;
|
|
759
|
+
const artifact = {
|
|
760
|
+
task_id: taskId,
|
|
761
|
+
prs,
|
|
762
|
+
...primary || {},
|
|
763
|
+
updated_at: nowIso()
|
|
764
|
+
};
|
|
765
|
+
writeFileSync(path, `${JSON.stringify(artifact, null, 2)}
|
|
766
|
+
`, "utf-8");
|
|
767
|
+
}
|
|
768
|
+
function readPrMetadata(projectRoot, taskId) {
|
|
769
|
+
const path = resolve3(taskData().artifactDirForId(projectRoot, taskId), "pr-state.json");
|
|
770
|
+
if (!existsSync4(path)) {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
775
|
+
if (!parsed || typeof parsed !== "object") {
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
if (parsed.prs && typeof parsed.prs === "object") {
|
|
779
|
+
return Object.values(parsed.prs).filter(isGitOpenPrResult);
|
|
780
|
+
}
|
|
781
|
+
return isGitOpenPrResult(parsed) ? [parsed] : [];
|
|
782
|
+
} catch {
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function isGitOpenPrResult(value) {
|
|
787
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
const record = value;
|
|
791
|
+
return typeof record.url === "string" && typeof record.branch === "string" && typeof record.base === "string" && (record.target === "project" || record.target === "monorepo") && typeof record.repoLabel === "string";
|
|
792
|
+
}
|
|
793
|
+
function resolveRepoRoot(projectRoot, target) {
|
|
794
|
+
return target === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
|
|
795
|
+
}
|
|
796
|
+
function ensureFullGitHistory(projectRoot, repoRoot, remoteName = "origin") {
|
|
797
|
+
const shallow = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--is-shallow-repository"), projectRoot);
|
|
798
|
+
if (shallow.exitCode !== 0 || shallow.stdout.trim() !== "true") {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const unshallow = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--unshallow", "--tags", remoteName), projectRoot);
|
|
802
|
+
if (unshallow.exitCode === 0) {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
const output = `${unshallow.stderr}
|
|
806
|
+
${unshallow.stdout}`.trim();
|
|
807
|
+
if (/--unshallow on a complete repository|does not make sense/i.test(output)) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
throw new Error(`Failed to expand git history for ${repoRoot}: ${output}`);
|
|
811
|
+
}
|
|
812
|
+
function refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner = "") {
|
|
813
|
+
const remoteName = resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner || resolveRepoNameWithOwner(projectRoot, repoRoot));
|
|
814
|
+
const remoteUrl = runCapture(gitCmd(projectRoot, repoRoot, "remote", "get-url", remoteName), projectRoot);
|
|
815
|
+
if (remoteUrl.exitCode !== 0) {
|
|
816
|
+
return "";
|
|
817
|
+
}
|
|
818
|
+
ensureFullGitHistory(projectRoot, repoRoot, remoteName);
|
|
819
|
+
const fetch2 = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--prune", "--tags", remoteName, `+refs/heads/${baseRef}:refs/remotes/${remoteName}/${baseRef}`), projectRoot);
|
|
820
|
+
if (fetch2.exitCode !== 0) {
|
|
821
|
+
throw new Error(`Failed to refresh ${remoteName}/${baseRef} at ${repoRoot}: ${fetch2.stderr || fetch2.stdout}`);
|
|
822
|
+
}
|
|
823
|
+
return remoteName;
|
|
824
|
+
}
|
|
825
|
+
function currentHeadMatchesMergedBase(projectRoot, repoRoot, baseRef, remoteName = "origin") {
|
|
826
|
+
const activeRemote = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef) || remoteName;
|
|
827
|
+
const remoteBase = `${activeRemote}/${baseRef}`;
|
|
828
|
+
const hasRemoteBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", remoteBase), repoRoot).exitCode === 0;
|
|
829
|
+
const targetRef = hasRemoteBase ? remoteBase : baseRef;
|
|
830
|
+
if (runCapture(gitCmd(projectRoot, repoRoot, "merge-base", "--is-ancestor", "HEAD", targetRef), repoRoot).exitCode === 0) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
return runCapture(gitCmd(projectRoot, repoRoot, "diff", "--quiet", "HEAD", targetRef, "--"), repoRoot).exitCode === 0;
|
|
834
|
+
}
|
|
835
|
+
function defaultReviewerForTask(projectRoot, taskId) {
|
|
836
|
+
if (!taskId) {
|
|
837
|
+
return "";
|
|
838
|
+
}
|
|
839
|
+
const entry = taskData().readTaskConfig(projectRoot)[taskId];
|
|
840
|
+
const reviewer = entry?.reviewer;
|
|
841
|
+
if (typeof reviewer === "string" && reviewer.trim()) {
|
|
842
|
+
return reviewer.trim();
|
|
843
|
+
}
|
|
844
|
+
const responsibleReviewer = entry?.responsible_reviewer;
|
|
845
|
+
if (typeof responsibleReviewer === "string" && responsibleReviewer.trim()) {
|
|
846
|
+
return responsibleReviewer.trim();
|
|
847
|
+
}
|
|
848
|
+
return "";
|
|
849
|
+
}
|
|
850
|
+
function resolveRepoNameWithOwner(projectRoot, repoRoot) {
|
|
851
|
+
const explicit = normalizeGithubRepoNameWithOwner(process.env.GH_REPO || "");
|
|
852
|
+
if (explicit) {
|
|
853
|
+
return explicit;
|
|
854
|
+
}
|
|
855
|
+
const visited = new Set;
|
|
856
|
+
return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
|
|
857
|
+
}
|
|
858
|
+
function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
|
|
859
|
+
const normalizedGitRoot = resolve3(gitRoot);
|
|
860
|
+
if (visited.has(normalizedGitRoot)) {
|
|
861
|
+
return "";
|
|
862
|
+
}
|
|
863
|
+
visited.add(normalizedGitRoot);
|
|
864
|
+
const remotes = listGitRemotes(projectRoot, gitRoot, cwd);
|
|
865
|
+
for (const remote of remotes) {
|
|
866
|
+
const urls = listGitRemoteUrls(projectRoot, gitRoot, cwd, remote);
|
|
867
|
+
for (const url of urls) {
|
|
868
|
+
const direct = normalizeGithubRepoNameWithOwner(url);
|
|
869
|
+
if (direct) {
|
|
870
|
+
return direct;
|
|
871
|
+
}
|
|
872
|
+
const localGitRoot = resolveLocalGitRemoteRoot(url, gitRoot);
|
|
873
|
+
if (!localGitRoot) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const viaMirror = resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, localGitRoot, cwd, visited);
|
|
877
|
+
if (viaMirror) {
|
|
878
|
+
return viaMirror;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return "";
|
|
883
|
+
}
|
|
884
|
+
function listGitRemotes(projectRoot, gitRoot, cwd) {
|
|
885
|
+
const result = gitQuery(projectRoot, gitRoot, cwd, "remote");
|
|
886
|
+
if (result.exitCode !== 0) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
890
|
+
}
|
|
891
|
+
function listGitRemoteUrls(projectRoot, gitRoot, cwd, remote) {
|
|
892
|
+
const result = gitQuery(projectRoot, gitRoot, cwd, "remote", "get-url", "--all", remote);
|
|
893
|
+
if (result.exitCode !== 0) {
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
897
|
+
}
|
|
898
|
+
function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
|
|
899
|
+
const remotes = listGitRemotes(projectRoot, repoRoot, repoRoot);
|
|
900
|
+
if (remotes.length === 0) {
|
|
901
|
+
return "origin";
|
|
902
|
+
}
|
|
903
|
+
if (!repoNameWithOwner) {
|
|
904
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
905
|
+
}
|
|
906
|
+
const expectedRepo = normalizeGithubRepoNameWithOwner(repoNameWithOwner).toLowerCase();
|
|
907
|
+
let directMatch = "";
|
|
908
|
+
for (const remote of remotes) {
|
|
909
|
+
const urls = listGitRemoteUrls(projectRoot, repoRoot, repoRoot, remote);
|
|
910
|
+
for (const url of urls) {
|
|
911
|
+
const direct = normalizeGithubRepoNameWithOwner(url);
|
|
912
|
+
if (direct && direct.toLowerCase() === expectedRepo) {
|
|
913
|
+
if (remote === "github") {
|
|
914
|
+
return remote;
|
|
915
|
+
}
|
|
916
|
+
if (!directMatch) {
|
|
917
|
+
directMatch = remote;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (remotes.includes("github")) {
|
|
923
|
+
return "github";
|
|
924
|
+
}
|
|
925
|
+
if (directMatch) {
|
|
926
|
+
return directMatch;
|
|
927
|
+
}
|
|
928
|
+
return remotes.includes("origin") ? "origin" : remotes[0];
|
|
929
|
+
}
|
|
930
|
+
function gitQuery(projectRoot, gitRoot, cwd, ...args) {
|
|
931
|
+
const gitArgs = existsSync4(resolve3(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveHostGitBinary(), "--git-dir", gitRoot, ...args];
|
|
932
|
+
return runCapture(gitArgs, cwd, projectRoot);
|
|
933
|
+
}
|
|
934
|
+
function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
|
|
935
|
+
const normalized = remoteUrl.trim();
|
|
936
|
+
if (!normalized) {
|
|
937
|
+
return "";
|
|
938
|
+
}
|
|
939
|
+
let candidate = normalized;
|
|
940
|
+
if (normalized.startsWith("file://")) {
|
|
941
|
+
try {
|
|
942
|
+
candidate = fileURLToPath(normalized);
|
|
943
|
+
} catch {
|
|
944
|
+
return "";
|
|
945
|
+
}
|
|
946
|
+
} else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
|
|
947
|
+
return "";
|
|
948
|
+
} else if (!isAbsolute(normalized)) {
|
|
949
|
+
candidate = resolve3(gitRoot, normalized);
|
|
950
|
+
}
|
|
951
|
+
return existsSync4(candidate) ? candidate : "";
|
|
952
|
+
}
|
|
953
|
+
function normalizeGithubRepoNameWithOwner(value) {
|
|
954
|
+
const normalized = value.trim();
|
|
955
|
+
if (!normalized) {
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
const scpMatch = normalized.match(/^(?:ssh:\/\/)?git@github\.com[:/](.+?)(?:\.git)?$/i);
|
|
959
|
+
if (scpMatch?.[1]) {
|
|
960
|
+
return scpMatch[1].replace(/^\/+|\/+$/g, "");
|
|
961
|
+
}
|
|
962
|
+
const httpMatch = normalized.match(/^https?:\/\/github\.com\/(.+?)(?:\.git)?(?:\/)?$/i);
|
|
963
|
+
if (httpMatch?.[1]) {
|
|
964
|
+
return httpMatch[1].replace(/^\/+|\/+$/g, "");
|
|
965
|
+
}
|
|
966
|
+
const bareMatch = normalized.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/);
|
|
967
|
+
return bareMatch ? bareMatch[0] : "";
|
|
968
|
+
}
|
|
969
|
+
function ghRepoArgs(repoNameWithOwner) {
|
|
970
|
+
return repoNameWithOwner ? ["-R", repoNameWithOwner] : [];
|
|
971
|
+
}
|
|
972
|
+
function withGhRepo(command, repoNameWithOwner) {
|
|
973
|
+
if (!repoNameWithOwner || command.length < 3) {
|
|
974
|
+
return command;
|
|
975
|
+
}
|
|
976
|
+
return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
|
|
977
|
+
}
|
|
978
|
+
function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
|
|
979
|
+
const remote = remoteName || "origin";
|
|
980
|
+
const symbolic = runCapture(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
|
|
981
|
+
if (symbolic.exitCode === 0) {
|
|
982
|
+
const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
|
|
983
|
+
if (ref && ref !== "HEAD") {
|
|
984
|
+
return ref;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const lsRemote = runCapture(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
|
|
988
|
+
if (lsRemote.exitCode === 0) {
|
|
989
|
+
const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
|
|
990
|
+
if (match?.[1]) {
|
|
991
|
+
return match[1];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
995
|
+
if (gh && repoNameWithOwner) {
|
|
996
|
+
const api = runCapture(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
|
|
997
|
+
const branch = api.exitCode === 0 ? api.stdout.trim() : "";
|
|
998
|
+
if (branch) {
|
|
999
|
+
return branch;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return fallback;
|
|
1003
|
+
}
|
|
1004
|
+
function inferProjectBase(projectRoot, fallback) {
|
|
1005
|
+
const containing = runCapture(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
|
|
1006
|
+
if (containing.exitCode !== 0) {
|
|
1007
|
+
return fallback;
|
|
1008
|
+
}
|
|
1009
|
+
const candidates = containing.stdout.split(/\r?\n/).map((line) => line.replace(/^\*/, "").trim()).filter(Boolean).map((line) => line.replace(/^origin\//, "")).filter((line) => line !== "HEAD" && !line.startsWith("rig/"));
|
|
1010
|
+
return candidates[0] || fallback;
|
|
1011
|
+
}
|
|
1012
|
+
function currentGithubLogin(repoRoot) {
|
|
1013
|
+
const gh = resolveGithubCliBinary({ scanPath: true });
|
|
1014
|
+
if (!gh) {
|
|
1015
|
+
return "";
|
|
1016
|
+
}
|
|
1017
|
+
const result = runCapture([gh, "api", "user", "--jq", ".login"], repoRoot);
|
|
1018
|
+
return result.exitCode === 0 ? result.stdout.trim() : "";
|
|
1019
|
+
}
|
|
1020
|
+
function collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
|
|
1021
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
|
|
1022
|
+
const remoteName = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner);
|
|
1023
|
+
const hasRemoteBase = remoteName ? runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", `${remoteName}/${baseRef}`), projectRoot).exitCode === 0 : false;
|
|
1024
|
+
const hasLocalBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", baseRef), projectRoot).exitCode === 0;
|
|
1025
|
+
let changed = "";
|
|
1026
|
+
if (hasRemoteBase) {
|
|
1027
|
+
changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${remoteName}/${baseRef}...${branchRef}`), projectRoot).stdout;
|
|
1028
|
+
} else if (hasLocalBase) {
|
|
1029
|
+
changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${baseRef}...${branchRef}`), projectRoot).stdout;
|
|
1030
|
+
} else {
|
|
1031
|
+
const fallback = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `HEAD~1..${branchRef}`), projectRoot);
|
|
1032
|
+
changed = fallback.exitCode === 0 ? fallback.stdout : runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only"), projectRoot).stdout;
|
|
1033
|
+
}
|
|
1034
|
+
return changed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 60);
|
|
1035
|
+
}
|
|
1036
|
+
function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
|
|
1037
|
+
const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
|
|
1038
|
+
if (!repoNameWithOwner) {
|
|
1039
|
+
return "";
|
|
1040
|
+
}
|
|
1041
|
+
const actorLogin = currentGithubLogin(repoRoot);
|
|
1042
|
+
const changedFiles = collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef);
|
|
1043
|
+
if (changedFiles.length === 0) {
|
|
1044
|
+
return "";
|
|
1045
|
+
}
|
|
1046
|
+
const counts = new Map;
|
|
1047
|
+
for (const path of changedFiles) {
|
|
1048
|
+
const result = runCapture([
|
|
1049
|
+
resolveGithubCliBinary({ scanPath: true }) || "gh",
|
|
1050
|
+
"api",
|
|
1051
|
+
`repos/${repoNameWithOwner}/commits`,
|
|
1052
|
+
"-f",
|
|
1053
|
+
`path=${path}`,
|
|
1054
|
+
"-f",
|
|
1055
|
+
`sha=${baseRef}`,
|
|
1056
|
+
"-f",
|
|
1057
|
+
"per_page=1",
|
|
1058
|
+
"--jq",
|
|
1059
|
+
".[0].author.login // empty"
|
|
1060
|
+
], repoRoot);
|
|
1061
|
+
const author = result.exitCode === 0 ? result.stdout.trim() : "";
|
|
1062
|
+
if (!author || author === actorLogin) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
counts.set(author, (counts.get(author) || 0) + 1);
|
|
1066
|
+
}
|
|
1067
|
+
let best = "";
|
|
1068
|
+
let max = -1;
|
|
1069
|
+
for (const [author, count] of counts.entries()) {
|
|
1070
|
+
if (count > max || count === max && author < best) {
|
|
1071
|
+
best = author;
|
|
1072
|
+
max = count;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
return best;
|
|
1076
|
+
}
|
|
1077
|
+
function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
|
|
1078
|
+
if (!existsSync4(resolve3(repo, ".git"))) {
|
|
1079
|
+
console.log(`Skipping ${label}: repo not available (${repo})`);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const scopedFiles = (files || []).filter(Boolean).filter((file) => !pathResolvesBeyondSymlink(repo, file));
|
|
1083
|
+
const repoChanges = changeCount(projectRoot, repo);
|
|
1084
|
+
if (scopedFiles.length === 0 && repoChanges === 0 && !allowEmpty) {
|
|
1085
|
+
console.log(`Skipping ${label}: no changes to commit.`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (scopedFiles.length > 0) {
|
|
1089
|
+
const indexFiles = new Set(runCapture(gitCmd(projectRoot, repo, "ls-files"), projectRoot).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
|
|
1090
|
+
const stageable = scopedFiles.filter((file) => indexFiles.has(file) || existsSync4(resolve3(repo, file)));
|
|
1091
|
+
if (stageable.length === 0) {
|
|
1092
|
+
console.log(`Skipping ${label}: collected change list matched no stageable paths in ${repo}.`);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const pathspecFile = resolve3(tmpdir(), `rig-stage-${process.pid}-${Date.now()}.txt`);
|
|
1096
|
+
writeFileSync(pathspecFile, `${stageable.join(`
|
|
1097
|
+
`)}
|
|
1098
|
+
`, "utf-8");
|
|
1099
|
+
try {
|
|
1100
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "add", `--pathspec-from-file=${pathspecFile}`), `Failed to stage changes for ${label}`);
|
|
1101
|
+
} finally {
|
|
1102
|
+
try {
|
|
1103
|
+
unlinkSync(pathspecFile);
|
|
1104
|
+
} catch {}
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
const addArgs = buildStageAddArgs(repo, scopedFiles, scoped);
|
|
1108
|
+
if (addArgs) {
|
|
1109
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, ...addArgs), `Failed to stage changes for ${label}`);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const stagedChanges = stagedChangeCount(projectRoot, repo);
|
|
1113
|
+
if (stagedChanges === 0) {
|
|
1114
|
+
if (allowEmpty) {
|
|
1115
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", "--allow-empty", "-m", message), `Failed to commit ${label}`);
|
|
1116
|
+
console.log(`Committed ${label}: ${message}`);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (scoped && repoChanges > 0) {
|
|
1120
|
+
const manifestHint = changedFilesManifest ? ` Refresh ${changedFilesManifest} and retry, or use --all/--unscoped intentionally.` : "";
|
|
1121
|
+
throw new Error(`Scoped commit for ${label} resolved no stageable files.${manifestHint}`);
|
|
1122
|
+
}
|
|
1123
|
+
console.log(`Skipping ${label}: no stageable changes to commit.`);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", ...allowEmpty ? ["--allow-empty"] : [], "-m", message), `Failed to commit ${label}`);
|
|
1127
|
+
console.log(`Committed ${label}: ${message}`);
|
|
1128
|
+
}
|
|
1129
|
+
function readChangedFilesManifest(projectRoot, taskId) {
|
|
1130
|
+
const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
1131
|
+
if (!existsSync4(manifestPath)) {
|
|
1132
|
+
return [];
|
|
1133
|
+
}
|
|
1134
|
+
const files = readFileSync3(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath(line)).filter(Boolean);
|
|
1135
|
+
return [...new Set(files)];
|
|
1136
|
+
}
|
|
1137
|
+
function refreshChangedFilesManifest(projectRoot, taskId) {
|
|
1138
|
+
const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
|
|
1139
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
1140
|
+
const changedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
|
|
1141
|
+
writeFileSync(manifestPath, `${changedFiles.join(`
|
|
1142
|
+
`)}
|
|
1143
|
+
`, "utf-8");
|
|
1144
|
+
return manifestPath;
|
|
1145
|
+
}
|
|
1146
|
+
function normalizeChangedFilePath(file) {
|
|
1147
|
+
return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1148
|
+
}
|
|
1149
|
+
function buildStageAddArgs(repoRoot, files, scoped) {
|
|
1150
|
+
if (files.length > 0) {
|
|
1151
|
+
return ["add", "--", ...files];
|
|
1152
|
+
}
|
|
1153
|
+
if (scoped) {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
return ["add", "-A", "--", ".", ...stageExcludePathspecs(repoRoot)];
|
|
1157
|
+
}
|
|
1158
|
+
function resolveScopedStageFilesForRepo(projectRoot, repoRoot, taskId, files) {
|
|
1159
|
+
const resolvedManifestFiles = resolveScopedFilesForRepo(projectRoot, repoRoot, files);
|
|
1160
|
+
if (resolvedManifestFiles.length > 0 || !taskId) {
|
|
1161
|
+
return resolvedManifestFiles;
|
|
1162
|
+
}
|
|
1163
|
+
return resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId);
|
|
1164
|
+
}
|
|
1165
|
+
function resolveScopedFilesForRepo(projectRoot, repoRoot, files) {
|
|
1166
|
+
const resolvedFiles = [];
|
|
1167
|
+
const seen = new Set;
|
|
1168
|
+
for (const file of files) {
|
|
1169
|
+
const candidate = resolveScopedRepoPath(repoRoot, file);
|
|
1170
|
+
if (!candidate || seen.has(candidate) || pathResolvesBeyondSymlink(repoRoot, candidate)) {
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
if (!repoHasPathChange(projectRoot, repoRoot, candidate)) {
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
seen.add(candidate);
|
|
1177
|
+
resolvedFiles.push(candidate);
|
|
1178
|
+
}
|
|
1179
|
+
return resolvedFiles;
|
|
1180
|
+
}
|
|
1181
|
+
function resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId) {
|
|
1182
|
+
const safeTaskId = safePathSegment(taskId, { fallback: "task", maxLength: 96 });
|
|
1183
|
+
const artifactPrefix = `artifacts/${safeTaskId}/`;
|
|
1184
|
+
const resolvedFiles = [];
|
|
1185
|
+
const seen = new Set;
|
|
1186
|
+
for (const file of collectRepoPendingFiles(projectRoot, repoRoot)) {
|
|
1187
|
+
if (!file.startsWith(artifactPrefix)) {
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
const artifactRelativePath = file.slice(artifactPrefix.length);
|
|
1191
|
+
if (!TASK_ARTIFACT_STAGE_FALLBACK.has(artifactRelativePath)) {
|
|
1192
|
+
continue;
|
|
1193
|
+
}
|
|
1194
|
+
if (seen.has(file) || pathResolvesBeyondSymlink(repoRoot, file)) {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
seen.add(file);
|
|
1198
|
+
resolvedFiles.push(file);
|
|
1199
|
+
}
|
|
1200
|
+
return resolvedFiles.sort();
|
|
1201
|
+
}
|
|
1202
|
+
function collectRepoPendingFiles(projectRoot, repoRoot) {
|
|
1203
|
+
const files = new Set;
|
|
1204
|
+
for (const args of [
|
|
1205
|
+
["diff", "--name-only"],
|
|
1206
|
+
["diff", "--cached", "--name-only"],
|
|
1207
|
+
["ls-files", "--others", "--exclude-standard"]
|
|
1208
|
+
]) {
|
|
1209
|
+
const result = runCapture(gitCmd(projectRoot, repoRoot, ...args), projectRoot);
|
|
1210
|
+
if (result.exitCode !== 0) {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
1214
|
+
const normalized = normalizeChangedFilePath(line);
|
|
1215
|
+
if (!normalized) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
files.add(normalized);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return [...files].sort();
|
|
1222
|
+
}
|
|
1223
|
+
function resolveScopedRepoPath(repoRoot, file) {
|
|
1224
|
+
const normalized = normalizeChangedFilePath(file);
|
|
1225
|
+
if (!normalized) {
|
|
1226
|
+
return "";
|
|
1227
|
+
}
|
|
1228
|
+
const rules = getScopeRules();
|
|
1229
|
+
if (rules?.stripPrefixes) {
|
|
1230
|
+
let result = normalized;
|
|
1231
|
+
for (const prefix of rules.stripPrefixes) {
|
|
1232
|
+
if (result.startsWith(prefix)) {
|
|
1233
|
+
result = result.slice(prefix.length);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
return result;
|
|
1237
|
+
}
|
|
1238
|
+
return normalized;
|
|
1239
|
+
}
|
|
1240
|
+
function repoHasPathChange(projectRoot, repoRoot, relativePath) {
|
|
1241
|
+
const result = runCapture(gitCmd(projectRoot, repoRoot, "status", "--short", "--", relativePath), projectRoot);
|
|
1242
|
+
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
1243
|
+
}
|
|
1244
|
+
function stageExcludePathspecs(repoRoot) {
|
|
1245
|
+
const patterns = existsSync4(resolve3(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
|
|
1246
|
+
return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
|
|
1247
|
+
}
|
|
1248
|
+
function pathResolvesBeyondSymlink(repoRoot, relativePath) {
|
|
1249
|
+
const parts = relativePath.split("/").filter(Boolean);
|
|
1250
|
+
if (parts.length <= 1) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
let current = repoRoot;
|
|
1254
|
+
for (let index = 0;index < parts.length - 1; index += 1) {
|
|
1255
|
+
current = resolve3(current, parts[index]);
|
|
1256
|
+
try {
|
|
1257
|
+
if (lstatSync(current).isSymbolicLink()) {
|
|
1258
|
+
return true;
|
|
1259
|
+
}
|
|
1260
|
+
} catch {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
function resolveTaskBranchId(projectRoot, taskId) {
|
|
1267
|
+
if (/^bd-[a-z0-9-]+$/.test(taskId)) {
|
|
1268
|
+
return taskId;
|
|
1269
|
+
}
|
|
1270
|
+
const normalizedTaskId = taskData().lookupTask(projectRoot, taskId);
|
|
1271
|
+
if (normalizedTaskId) {
|
|
1272
|
+
return normalizedTaskId;
|
|
1273
|
+
}
|
|
1274
|
+
const currentTask = taskData().currentTaskId(projectRoot);
|
|
1275
|
+
if (currentTask && currentTask === taskId) {
|
|
1276
|
+
return currentTask;
|
|
1277
|
+
}
|
|
1278
|
+
const runtimeIdFromEnv = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
|
|
1279
|
+
if (runtimeIdFromEnv.startsWith("task-") && runtimeIdFromEnv.length > "task-".length) {
|
|
1280
|
+
return runtimeIdFromEnv.slice("task-".length);
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
const runtimeIdFromContext = loadRuntimeContextFromEnv()?.runtimeId || "";
|
|
1284
|
+
if (runtimeIdFromContext.startsWith("task-") && runtimeIdFromContext.length > "task-".length) {
|
|
1285
|
+
return runtimeIdFromContext.slice("task-".length);
|
|
1286
|
+
}
|
|
1287
|
+
} catch {}
|
|
1288
|
+
const artifactDir = taskData().artifactDirForId(projectRoot, taskId);
|
|
1289
|
+
if (existsSync4(artifactDir)) {
|
|
1290
|
+
return taskId;
|
|
1291
|
+
}
|
|
1292
|
+
throw new Error(`Unknown task id: ${taskId}`);
|
|
1293
|
+
}
|
|
1294
|
+
function branchName(projectRoot, repo) {
|
|
1295
|
+
return runCapture(gitCmd(projectRoot, repo, "rev-parse", "--abbrev-ref", "HEAD"), projectRoot).stdout.trim();
|
|
1296
|
+
}
|
|
1297
|
+
function changeCount(projectRoot, repo) {
|
|
1298
|
+
const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
|
|
1299
|
+
return status ? status.split(/\r?\n/).filter(Boolean).length : 0;
|
|
1300
|
+
}
|
|
1301
|
+
function stagedChangeCount(projectRoot, repo) {
|
|
1302
|
+
const staged = runCapture(gitCmd(projectRoot, repo, "diff", "--cached", "--name-only"), projectRoot).stdout.trim();
|
|
1303
|
+
return staged ? staged.split(/\r?\n/).filter(Boolean).length : 0;
|
|
1304
|
+
}
|
|
1305
|
+
function runOrThrow(projectRoot, command, errorPrefix) {
|
|
1306
|
+
const result = runCapture(command, projectRoot);
|
|
1307
|
+
if (result.exitCode !== 0) {
|
|
1308
|
+
throw new Error(`${errorPrefix}:
|
|
1309
|
+
${result.stderr || result.stdout}`);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function runCapture(command, cwd, projectRoot = cwd) {
|
|
1313
|
+
return baseRunCapture(command, cwd, runtimeGitEnv(projectRoot));
|
|
1314
|
+
}
|
|
1315
|
+
function runtimeGitEnv(projectRoot) {
|
|
1316
|
+
const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
|
|
1317
|
+
const runtimeHome = runtimeRoot ? resolve3(runtimeRoot, "home") : "";
|
|
1318
|
+
const runtimeTmp = runtimeRoot ? resolve3(runtimeRoot, "tmp") : "";
|
|
1319
|
+
const runtimeCache = runtimeRoot ? resolve3(runtimeRoot, "cache") : "";
|
|
1320
|
+
const runtimeKnownHosts = runtimeHome ? resolve3(runtimeHome, ".ssh", "known_hosts") : "";
|
|
1321
|
+
const runtimeKey = runtimeHome ? resolve3(runtimeHome, ".ssh", "rig-agent-key") : "";
|
|
1322
|
+
const env = {};
|
|
1323
|
+
if (ctx?.workspaceDir) {
|
|
1324
|
+
env.PROJECT_RIG_ROOT = projectRoot;
|
|
1325
|
+
env.RIG_TASK_WORKSPACE = ctx.workspaceDir;
|
|
1326
|
+
env.MONOREPO_ROOT = ctx.workspaceDir;
|
|
1327
|
+
env.MONOREPO_MAIN_ROOT = resolveMonorepoRoot(projectRoot);
|
|
1328
|
+
} else if (projectRoot) {
|
|
1329
|
+
env.PROJECT_RIG_ROOT = projectRoot;
|
|
1330
|
+
}
|
|
1331
|
+
if (runtimeRoot) {
|
|
1332
|
+
env.RIG_RUNTIME_HOME = runtimeRoot;
|
|
1333
|
+
}
|
|
1334
|
+
if (runtimeHome && existsSync4(runtimeHome)) {
|
|
1335
|
+
env.HOME = runtimeHome;
|
|
1336
|
+
env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
|
|
1337
|
+
}
|
|
1338
|
+
if (runtimeTmp && existsSync4(runtimeTmp)) {
|
|
1339
|
+
env.TMPDIR = runtimeTmp;
|
|
1340
|
+
}
|
|
1341
|
+
if (runtimeCache && existsSync4(runtimeCache)) {
|
|
1342
|
+
env.XDG_CACHE_HOME = runtimeCache;
|
|
1343
|
+
}
|
|
1344
|
+
const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
|
|
1345
|
+
for (const [key, value] of Object.entries(resolveRuntimeSecrets(process.env, workspaceSecrets))) {
|
|
1346
|
+
if (key === "GITHUB_SSH_KEY" || !value) {
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
env[key] = value;
|
|
1350
|
+
}
|
|
1351
|
+
const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
|
|
1352
|
+
if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
1353
|
+
env.GITHUB_TOKEN = rigGithubToken;
|
|
1354
|
+
}
|
|
1355
|
+
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
1356
|
+
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
1357
|
+
}
|
|
1358
|
+
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
1359
|
+
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
1360
|
+
}
|
|
1361
|
+
if (!env.GREPTILE_GITHUB_TOKEN && env.GITHUB_TOKEN) {
|
|
1362
|
+
env.GREPTILE_GITHUB_TOKEN = env.GITHUB_TOKEN;
|
|
1363
|
+
}
|
|
1364
|
+
const persistedSecrets = loadPersistedRuntimeSecrets(runtimeRoot);
|
|
1365
|
+
for (const [key, value] of Object.entries(persistedSecrets)) {
|
|
1366
|
+
if (!value)
|
|
1367
|
+
continue;
|
|
1368
|
+
if (!env[key]) {
|
|
1369
|
+
env[key] = value;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
|
|
1373
|
+
env.GITHUB_TOKEN = env.GH_TOKEN;
|
|
1374
|
+
}
|
|
1375
|
+
if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
|
|
1376
|
+
env.GH_TOKEN = env.GITHUB_TOKEN;
|
|
1377
|
+
}
|
|
1378
|
+
const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
|
|
1379
|
+
if (gitHubToken) {
|
|
1380
|
+
env.RIG_GITHUB_TOKEN = gitHubToken;
|
|
1381
|
+
env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
|
|
1382
|
+
env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
|
|
1383
|
+
applyGitHubCredentialHelperEnv(env);
|
|
1384
|
+
}
|
|
1385
|
+
if (runtimeKnownHosts && existsSync4(runtimeKnownHosts)) {
|
|
1386
|
+
const sshParts = [
|
|
1387
|
+
"ssh",
|
|
1388
|
+
`-o UserKnownHostsFile="${runtimeKnownHosts}"`,
|
|
1389
|
+
"-o StrictHostKeyChecking=yes",
|
|
1390
|
+
"-F /dev/null"
|
|
1391
|
+
];
|
|
1392
|
+
if (runtimeKey && existsSync4(runtimeKey)) {
|
|
1393
|
+
sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
|
|
1394
|
+
}
|
|
1395
|
+
env.GIT_SSH_COMMAND = sshParts.join(" ");
|
|
1396
|
+
} else if (process.env.GIT_SSH_COMMAND?.trim()) {
|
|
1397
|
+
env.GIT_SSH_COMMAND = process.env.GIT_SSH_COMMAND;
|
|
1398
|
+
}
|
|
1399
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
1400
|
+
}
|
|
1401
|
+
function applyGitHubCredentialHelperEnv(env) {
|
|
1402
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
1403
|
+
env.GIT_CONFIG_COUNT = "2";
|
|
1404
|
+
env.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
1405
|
+
env.GIT_CONFIG_VALUE_0 = "";
|
|
1406
|
+
env.GIT_CONFIG_KEY_1 = "credential.helper";
|
|
1407
|
+
env.GIT_CONFIG_VALUE_1 = '!f() { test "$1" = get || exit 0; token="${GITHUB_TOKEN:-${GH_TOKEN:-${RIG_GITHUB_TOKEN:-}}}"; test -n "$token" || exit 0; echo username=x-access-token; echo password="$token"; }; f';
|
|
1408
|
+
}
|
|
1409
|
+
function loadPersistedRuntimeSecrets(runtimeRoot) {
|
|
1410
|
+
if (!runtimeRoot) {
|
|
1411
|
+
return {};
|
|
1412
|
+
}
|
|
1413
|
+
const path = resolve3(runtimeRoot, "runtime-secrets.json");
|
|
1414
|
+
if (!existsSync4(path)) {
|
|
1415
|
+
return {};
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
1419
|
+
const allowed = new Set(["GITHUB_TOKEN", "GH_TOKEN", "RIG_GITHUB_TOKEN"]);
|
|
1420
|
+
const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string" && allowed.has(entry[0]));
|
|
1421
|
+
return Object.fromEntries(entries);
|
|
1422
|
+
} catch {
|
|
1423
|
+
return {};
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
function ensureRuntimeOpenSslConfig(runtimeHome) {
|
|
1427
|
+
const sslDir = resolve3(runtimeHome, ".ssl");
|
|
1428
|
+
const sslConfig = resolve3(sslDir, "openssl.cnf");
|
|
1429
|
+
if (!existsSync4(sslDir)) {
|
|
1430
|
+
mkdirSync(sslDir, { recursive: true });
|
|
1431
|
+
}
|
|
1432
|
+
if (!existsSync4(sslConfig)) {
|
|
1433
|
+
writeFileSync(sslConfig, `# Rig runtime OpenSSL config placeholder
|
|
1434
|
+
`);
|
|
1435
|
+
}
|
|
1436
|
+
return sslConfig;
|
|
1437
|
+
}
|
|
1438
|
+
function resolveRuntimeMetadata(projectRoot) {
|
|
1439
|
+
const contextFile = process.env.RIG_RUNTIME_CONTEXT_FILE?.trim();
|
|
1440
|
+
const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
|
|
1441
|
+
let ctx = loadRuntimeContextFromEnv();
|
|
1442
|
+
if (runtimeHome) {
|
|
1443
|
+
return {
|
|
1444
|
+
ctx,
|
|
1445
|
+
runtimeRoot: runtimeHome
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
if (contextFile) {
|
|
1449
|
+
return {
|
|
1450
|
+
ctx,
|
|
1451
|
+
runtimeRoot: dirname(resolve3(contextFile))
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
const inferredContextFile = findRuntimeContextFile(projectRoot);
|
|
1455
|
+
if (existsSync4(inferredContextFile)) {
|
|
1456
|
+
try {
|
|
1457
|
+
ctx = loadRuntimeContext(inferredContextFile);
|
|
1458
|
+
} catch {}
|
|
1459
|
+
return {
|
|
1460
|
+
ctx,
|
|
1461
|
+
runtimeRoot: dirname(inferredContextFile)
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
return { ctx, runtimeRoot: "" };
|
|
1465
|
+
}
|
|
1466
|
+
function findRuntimeContextFile(startPath) {
|
|
1467
|
+
let current = resolve3(startPath);
|
|
1468
|
+
while (true) {
|
|
1469
|
+
const candidate = resolve3(current, "runtime-context.json");
|
|
1470
|
+
if (existsSync4(candidate)) {
|
|
1471
|
+
return candidate;
|
|
1472
|
+
}
|
|
1473
|
+
const parent = dirname(current);
|
|
1474
|
+
if (parent === current) {
|
|
1475
|
+
return "";
|
|
1476
|
+
}
|
|
1477
|
+
current = parent;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// packages/bundle-default-lifecycle/src/control-plane/pr-merge-gate-cap.ts
|
|
1482
|
+
import { PR_MERGE_GATE } from "@rig/contracts";
|
|
1483
|
+
import { defineCapability as defineCapability2 } from "@rig/core/capability";
|
|
1484
|
+
import { resolvePluginHost } from "@rig/core/project-plugins";
|
|
1485
|
+
var PrMergeGateCap = defineCapability2(PR_MERGE_GATE);
|
|
1486
|
+
async function resolvePrMergeGateService(projectRoot) {
|
|
1487
|
+
const { host } = await resolvePluginHost(projectRoot);
|
|
1488
|
+
return PrMergeGateCap.require(host);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// packages/bundle-default-lifecycle/src/control-plane/verifier.ts
|
|
1492
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1493
|
+
import { resolve as resolve4 } from "path";
|
|
1494
|
+
import { resolveRuntimeSecrets as resolveRuntimeSecrets2 } from "@rig/core/baked-secrets";
|
|
1495
|
+
import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/core/runtime-context";
|
|
1496
|
+
import { nowIso as nowIso2, runCapture as runCapture2 } from "@rig/core/exec";
|
|
1497
|
+
import { resolveHarnessPaths } from "@rig/core/harness-paths";
|
|
1498
|
+
var mergeGateHolder = null;
|
|
1499
|
+
async function ensureMergeGate(projectRoot) {
|
|
1500
|
+
mergeGateHolder = await resolvePrMergeGateService(projectRoot);
|
|
1501
|
+
return mergeGateHolder;
|
|
1502
|
+
}
|
|
1503
|
+
function mg() {
|
|
1504
|
+
if (!mergeGateHolder) {
|
|
1505
|
+
throw new Error("PR merge-gate capability not resolved (verifyTask must run first).");
|
|
1506
|
+
}
|
|
1507
|
+
return mergeGateHolder;
|
|
1508
|
+
}
|
|
35
1509
|
async function verifyTask(options) {
|
|
1510
|
+
await ensureMergeGate(options.projectRoot);
|
|
36
1511
|
const paths = resolveHarnessPaths(options.projectRoot);
|
|
37
1512
|
const taskId = options.taskId;
|
|
38
|
-
const normalizedTaskId = lookupTask(options.projectRoot, taskId);
|
|
39
|
-
const artifactDir = artifactDirForId(options.projectRoot, taskId);
|
|
40
|
-
|
|
41
|
-
const validationSummaryPath =
|
|
42
|
-
const reviewFeedbackPath =
|
|
43
|
-
const reviewStatePath =
|
|
44
|
-
const greptileRawPath =
|
|
1513
|
+
const normalizedTaskId = taskData().lookupTask(options.projectRoot, taskId);
|
|
1514
|
+
const artifactDir = taskData().artifactDirForId(options.projectRoot, taskId);
|
|
1515
|
+
mkdirSync2(artifactDir, { recursive: true });
|
|
1516
|
+
const validationSummaryPath = resolve4(artifactDir, "validation-summary.json");
|
|
1517
|
+
const reviewFeedbackPath = resolve4(artifactDir, "review-feedback.md");
|
|
1518
|
+
const reviewStatePath = resolve4(artifactDir, "review-state.json");
|
|
1519
|
+
const greptileRawPath = resolve4(artifactDir, "review-greptile-raw.json");
|
|
45
1520
|
const prStates = readPrMetadata(options.projectRoot, taskId);
|
|
46
1521
|
const prState = prStates[0] || null;
|
|
47
1522
|
const localReasons = [];
|
|
@@ -53,7 +1528,7 @@ async function verifyTask(options) {
|
|
|
53
1528
|
if (!normalizedTaskId && !await hasConfiguredSourceTask(options.projectRoot, taskId)) {
|
|
54
1529
|
localReasons.push(`[Task Config] Unknown task id '${taskId}' in task-config or configured task source.`);
|
|
55
1530
|
}
|
|
56
|
-
if (!
|
|
1531
|
+
if (!existsSync5(validationSummaryPath)) {
|
|
57
1532
|
localReasons.push(`[Artifact Quality] validation-summary.json not found at ${validationSummaryPath}.`);
|
|
58
1533
|
} else {
|
|
59
1534
|
const summary = await parseValidationSummary(validationSummaryPath);
|
|
@@ -62,13 +1537,13 @@ async function verifyTask(options) {
|
|
|
62
1537
|
}
|
|
63
1538
|
}
|
|
64
1539
|
for (const file of ["task-result.json", "decision-log.md", "next-actions.md", "changed-files.txt"]) {
|
|
65
|
-
const requiredPath =
|
|
66
|
-
if (!
|
|
1540
|
+
const requiredPath = resolve4(artifactDir, file);
|
|
1541
|
+
if (!existsSync5(requiredPath)) {
|
|
67
1542
|
localReasons.push(`[Artifact Quality] Missing required artifact file: ${requiredPath}`);
|
|
68
1543
|
}
|
|
69
1544
|
}
|
|
70
|
-
const taskResultPath =
|
|
71
|
-
if (
|
|
1545
|
+
const taskResultPath = resolve4(artifactDir, "task-result.json");
|
|
1546
|
+
if (existsSync5(taskResultPath)) {
|
|
72
1547
|
const taskResult = await readJsonFile(taskResultPath);
|
|
73
1548
|
const artifactStatus = typeof taskResult?.status === "string" ? taskResult.status.trim().toLowerCase() : "";
|
|
74
1549
|
if (artifactStatus === "partial") {
|
|
@@ -81,8 +1556,8 @@ async function verifyTask(options) {
|
|
|
81
1556
|
localReasons.push("[Artifact Quality] task-result.json next actions indicate remaining implementation scope.");
|
|
82
1557
|
}
|
|
83
1558
|
}
|
|
84
|
-
const nextActionsPath =
|
|
85
|
-
if (
|
|
1559
|
+
const nextActionsPath = resolve4(artifactDir, "next-actions.md");
|
|
1560
|
+
if (existsSync5(nextActionsPath)) {
|
|
86
1561
|
const nextActionsContent = await Bun.file(nextActionsPath).text();
|
|
87
1562
|
if (nextActionsContent.includes("TODO: Replace this scaffold") || nextActionsContent.includes("bd-<downstream-task-id>")) {
|
|
88
1563
|
localReasons.push("[Artifact Quality] next-actions.md still contains scaffold placeholder text. Replace with real recommendations.");
|
|
@@ -113,7 +1588,7 @@ async function verifyTask(options) {
|
|
|
113
1588
|
aiReasons.push(`[AI Review] Required mode needs a completed Greptile approval; current verdict is ${ai.verdict}.`);
|
|
114
1589
|
}
|
|
115
1590
|
if (persistArtifacts && ai.rawResponse) {
|
|
116
|
-
|
|
1591
|
+
writeFileSync2(greptileRawPath, `${ai.rawResponse}
|
|
117
1592
|
`, "utf-8");
|
|
118
1593
|
}
|
|
119
1594
|
} else if (!options.skipAiReview && reviewMode === "off") {
|
|
@@ -235,15 +1710,15 @@ function nextActionsIndicateRemainingScope(content) {
|
|
|
235
1710
|
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);
|
|
236
1711
|
}
|
|
237
1712
|
async function hasConfiguredSourceTask(projectRoot, taskId) {
|
|
238
|
-
return readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
1713
|
+
return taskData().readConfiguredTaskSourceTask(projectRoot, taskId).then((result) => result.task !== null).catch(() => false);
|
|
239
1714
|
}
|
|
240
1715
|
function resolveGithubSourceIssueId(projectRoot, taskId) {
|
|
241
|
-
const fromRuntime =
|
|
1716
|
+
const fromRuntime = loadRuntimeContextFromEnv2()?.sourceTask?.sourceIssueId;
|
|
242
1717
|
if (typeof fromRuntime === "string" && isGithubSourceIssueId(fromRuntime)) {
|
|
243
1718
|
return fromRuntime;
|
|
244
1719
|
}
|
|
245
1720
|
try {
|
|
246
|
-
const taskConfig = readTaskConfig(projectRoot);
|
|
1721
|
+
const taskConfig = taskData().readTaskConfig(projectRoot);
|
|
247
1722
|
const entry = taskConfig[taskId];
|
|
248
1723
|
const sourceIssueId = typeof entry?.sourceIssueId === "string" ? entry.sourceIssueId : typeof entry?.source_issue_id === "string" ? entry.source_issue_id : null;
|
|
249
1724
|
if (sourceIssueId && isGithubSourceIssueId(sourceIssueId)) {
|
|
@@ -314,14 +1789,15 @@ function loadGithubPullRequestCloseoutSnapshot(projectRoot, prState) {
|
|
|
314
1789
|
"--json",
|
|
315
1790
|
"state,isDraft,mergeable,mergeStateStatus,reviewDecision,title,body,statusCheckRollup"
|
|
316
1791
|
]);
|
|
1792
|
+
const isDraft = booleanField(view, "isDraft");
|
|
317
1793
|
return {
|
|
318
|
-
state
|
|
319
|
-
isDraft
|
|
320
|
-
mergeable
|
|
321
|
-
mergeStateStatus
|
|
322
|
-
reviewDecision
|
|
323
|
-
title
|
|
324
|
-
body
|
|
1794
|
+
...objectField("state", stringField(view, "state")),
|
|
1795
|
+
...isDraft !== undefined ? { isDraft } : {},
|
|
1796
|
+
...objectField("mergeable", stringField(view, "mergeable")),
|
|
1797
|
+
...objectField("mergeStateStatus", stringField(view, "mergeStateStatus")),
|
|
1798
|
+
...objectField("reviewDecision", stringField(view, "reviewDecision")),
|
|
1799
|
+
...objectField("title", stringField(view, "title")),
|
|
1800
|
+
...objectField("body", stringField(view, "body")),
|
|
325
1801
|
statusCheckRollup: statusCheckRollupField(view, "statusCheckRollup"),
|
|
326
1802
|
reviewThreads: loadGithubReviewThreads(projectRoot, repoName, prNumber)
|
|
327
1803
|
};
|
|
@@ -396,6 +1872,9 @@ function stringField(record, key) {
|
|
|
396
1872
|
const value = record[key];
|
|
397
1873
|
return typeof value === "string" ? value : undefined;
|
|
398
1874
|
}
|
|
1875
|
+
function objectField(key, value) {
|
|
1876
|
+
return value === undefined ? {} : { [key]: value };
|
|
1877
|
+
}
|
|
399
1878
|
function booleanField(record, key) {
|
|
400
1879
|
const value = record[key];
|
|
401
1880
|
return typeof value === "boolean" ? value : undefined;
|
|
@@ -452,7 +1931,7 @@ function isAcceptedValidationSummary(summary) {
|
|
|
452
1931
|
return summary.status === "skipped" && summary.total === 0 && summary.failed === 0;
|
|
453
1932
|
}
|
|
454
1933
|
async function loadReviewMode(reviewProfilePath, fallback) {
|
|
455
|
-
const parsed =
|
|
1934
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
456
1935
|
const mode = parsed?.mode;
|
|
457
1936
|
if (mode === "off" || mode === "advisory" || mode === "required") {
|
|
458
1937
|
return mode;
|
|
@@ -463,7 +1942,7 @@ async function loadReviewMode(reviewProfilePath, fallback) {
|
|
|
463
1942
|
return "advisory";
|
|
464
1943
|
}
|
|
465
1944
|
async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
466
|
-
const parsed =
|
|
1945
|
+
const parsed = existsSync5(reviewProfilePath) ? await readJsonFile(reviewProfilePath) : null;
|
|
467
1946
|
const provider = parsed?.provider;
|
|
468
1947
|
if (typeof provider === "string" && provider.trim().length > 0) {
|
|
469
1948
|
return provider;
|
|
@@ -472,7 +1951,7 @@ async function loadReviewProvider(reviewProfilePath, fallback) {
|
|
|
472
1951
|
}
|
|
473
1952
|
function resolveRepoSlug(projectRoot) {
|
|
474
1953
|
const paths = resolveHarnessPaths(projectRoot);
|
|
475
|
-
const remote =
|
|
1954
|
+
const remote = runCapture2(["git", "-C", paths.monorepoRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim() || runCapture2(["git", "-C", projectRoot, "remote", "get-url", "origin"], projectRoot).stdout.trim();
|
|
476
1955
|
if (!remote) {
|
|
477
1956
|
return "";
|
|
478
1957
|
}
|
|
@@ -486,7 +1965,7 @@ function resolveRepoSlug(projectRoot) {
|
|
|
486
1965
|
async function runGreptileReview(options) {
|
|
487
1966
|
const reasons = [];
|
|
488
1967
|
const warnings = [];
|
|
489
|
-
const secrets =
|
|
1968
|
+
const secrets = resolveRuntimeSecrets2(process.env);
|
|
490
1969
|
const apiKey = secrets.GREPTILE_API_KEY || "";
|
|
491
1970
|
const apiBase = secrets.GREPTILE_API_BASE || "https://api.greptile.com/mcp";
|
|
492
1971
|
const remote = secrets.GREPTILE_REMOTE || "github";
|
|
@@ -622,7 +2101,7 @@ function writeFeedbackFile(options) {
|
|
|
622
2101
|
if (options.aiRawFeedback) {
|
|
623
2102
|
lines.push("## Raw Reviewer Feedback", "", "```text", options.aiRawFeedback, "```", "");
|
|
624
2103
|
}
|
|
625
|
-
|
|
2104
|
+
writeFileSync2(options.output, `${lines.join(`
|
|
626
2105
|
`)}
|
|
627
2106
|
`, "utf-8");
|
|
628
2107
|
}
|
|
@@ -637,9 +2116,9 @@ function writeReviewStateFile(options) {
|
|
|
637
2116
|
local_reasons: options.localReasons,
|
|
638
2117
|
ai_reasons: options.aiReasons,
|
|
639
2118
|
ai_warnings: options.aiWarnings,
|
|
640
|
-
updated_at:
|
|
2119
|
+
updated_at: nowIso2()
|
|
641
2120
|
};
|
|
642
|
-
|
|
2121
|
+
writeFileSync2(options.output, `${JSON.stringify(payload, null, 2)}
|
|
643
2122
|
`, "utf-8");
|
|
644
2123
|
}
|
|
645
2124
|
async function runGreptileReviewForPr(options) {
|
|
@@ -664,7 +2143,6 @@ async function runGreptileReviewForPr(options) {
|
|
|
664
2143
|
taskId: options.taskId,
|
|
665
2144
|
prState: options.prState,
|
|
666
2145
|
reviewMode: options.reviewMode,
|
|
667
|
-
infrastructureError: undefined,
|
|
668
2146
|
pollAttempts: options.pollAttempts,
|
|
669
2147
|
pollIntervalMs: options.pollIntervalMs
|
|
670
2148
|
});
|
|
@@ -792,7 +2270,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
792
2270
|
});
|
|
793
2271
|
const actionableComments = filterActionableGreptileComments(commentsPayload.comments || []);
|
|
794
2272
|
const reviewBody = reviewDetails.codeReview?.body || "";
|
|
795
|
-
const score = parseGreptileScore(reviewBody);
|
|
2273
|
+
const score = mg().parseGreptileScore(reviewBody);
|
|
796
2274
|
const feedback = [
|
|
797
2275
|
`## ${options.prState.repoLabel || repoName} PR Review`,
|
|
798
2276
|
"",
|
|
@@ -800,7 +2278,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
800
2278
|
`- Review ID: ${selectedReview.id}`,
|
|
801
2279
|
`- Status: ${selectedReview.status}`,
|
|
802
2280
|
"",
|
|
803
|
-
reviewBody ? stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
2281
|
+
reviewBody ? mg().stripHtml(reviewBody).trim() : "Greptile completed without summary body."
|
|
804
2282
|
].filter(Boolean).join(`
|
|
805
2283
|
`);
|
|
806
2284
|
if (actionableComments.length > 0) {
|
|
@@ -821,7 +2299,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
821
2299
|
}
|
|
822
2300
|
};
|
|
823
2301
|
}
|
|
824
|
-
const blockerScanBody = stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
2302
|
+
const blockerScanBody = mg().stripHtml(reviewBody).replace(/\b(?:no|without|zero)\s+blockers?\b/gi, " ").replace(/\bno\s+changes\s+requested\b/gi, " ");
|
|
825
2303
|
if (/not safe(?: to merge)?|unsafe(?: to merge)?|do not merge|cannot merge|blockers?|must fix|changes requested|please fix|needs? fix|fix this|address this|\breject(?:ed|ion)?\b|\bskip(?:ped)?\b|status\s*:\s*(?:reject(?:ed)?|skip(?:ped)?|failed)/i.test(blockerScanBody)) {
|
|
826
2304
|
reasons.push(`[AI Review] ${repoName}#${prNumber} summary indicates the PR is not safe to merge.`);
|
|
827
2305
|
return {
|
|
@@ -869,7 +2347,7 @@ async function runGreptileReviewForPr(options) {
|
|
|
869
2347
|
status: selectedReview.status
|
|
870
2348
|
}]
|
|
871
2349
|
});
|
|
872
|
-
strictGate =
|
|
2350
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
873
2351
|
} catch (error) {
|
|
874
2352
|
reasons.push(`[AI Review] Strict Greptile evidence collection failed for ${repoName}#${prNumber}: ${error instanceof Error ? error.message : String(error)}`);
|
|
875
2353
|
return {
|
|
@@ -996,7 +2474,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
996
2474
|
fallbackReview?.html_url ? `- Review: ${fallbackReview.html_url}` : "",
|
|
997
2475
|
fallbackReview?.state ? `- Status: ${fallbackReview.state}` : "",
|
|
998
2476
|
"",
|
|
999
|
-
fallbackReview?.body?.trim() ? stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
|
|
2477
|
+
fallbackReview?.body?.trim() ? mg().stripHtml(fallbackReview.body).trim() : "Greptile MCP was unavailable, so verification used GitHub review threads instead."
|
|
1000
2478
|
].filter(Boolean).join(`
|
|
1001
2479
|
`);
|
|
1002
2480
|
const warnings = buildGithubGreptileFallbackWarnings(options);
|
|
@@ -1019,7 +2497,7 @@ async function runGithubGreptileFallbackReviewForPr(options) {
|
|
|
1019
2497
|
taskId: options.taskId,
|
|
1020
2498
|
prUrl
|
|
1021
2499
|
});
|
|
1022
|
-
strictGate =
|
|
2500
|
+
strictGate = mg().evaluateGate(strictEvidence);
|
|
1023
2501
|
} catch (error) {
|
|
1024
2502
|
return {
|
|
1025
2503
|
verdict: "REJECT",
|
|
@@ -1244,7 +2722,7 @@ function loadGithubPullRequestState(projectRoot, repoName, prNumber) {
|
|
|
1244
2722
|
]);
|
|
1245
2723
|
return {
|
|
1246
2724
|
state: response.state || "",
|
|
1247
|
-
merged: response.merged,
|
|
2725
|
+
...response.merged !== undefined ? { merged: response.merged } : {},
|
|
1248
2726
|
merged_at: response.merged_at ?? null
|
|
1249
2727
|
};
|
|
1250
2728
|
}
|
|
@@ -1262,7 +2740,7 @@ function parsePullRequestNumber(url) {
|
|
|
1262
2740
|
return match ? Number.parseInt(match[1] || "0", 10) : 0;
|
|
1263
2741
|
}
|
|
1264
2742
|
function runGhJson(projectRoot, args) {
|
|
1265
|
-
const result =
|
|
2743
|
+
const result = runCapture2(["gh", ...args], projectRoot);
|
|
1266
2744
|
if (result.exitCode !== 0) {
|
|
1267
2745
|
throw new Error(result.stderr || result.stdout || `gh ${args.join(" ")} failed`);
|
|
1268
2746
|
}
|
|
@@ -1273,7 +2751,7 @@ function runGhJson(projectRoot, args) {
|
|
|
1273
2751
|
}
|
|
1274
2752
|
}
|
|
1275
2753
|
async function collectStrictPrEvidenceForVerifier(input) {
|
|
1276
|
-
return
|
|
2754
|
+
return mg().collectEvidence({
|
|
1277
2755
|
projectRoot: input.projectRoot,
|
|
1278
2756
|
prUrl: input.prUrl,
|
|
1279
2757
|
taskId: input.taskId,
|
|
@@ -1281,7 +2759,7 @@ async function collectStrictPrEvidenceForVerifier(input) {
|
|
|
1281
2759
|
cycle: 0,
|
|
1282
2760
|
apiSignals: input.apiSignals ?? [],
|
|
1283
2761
|
command: async (args, options) => {
|
|
1284
|
-
const result =
|
|
2762
|
+
const result = runCapture2(["gh", ...args], options?.cwd ?? input.projectRoot);
|
|
1285
2763
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
1286
2764
|
}
|
|
1287
2765
|
});
|
|
@@ -1294,11 +2772,11 @@ function deriveRepoName(projectRoot, prState) {
|
|
|
1294
2772
|
if (prState.target === "monorepo") {
|
|
1295
2773
|
return resolveRepoSlug(projectRoot);
|
|
1296
2774
|
}
|
|
1297
|
-
return
|
|
2775
|
+
return runCapture2(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"], projectRoot).stdout.trim();
|
|
1298
2776
|
}
|
|
1299
2777
|
function resolvePrHeadSha(projectRoot, prState) {
|
|
1300
2778
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
1301
|
-
return
|
|
2779
|
+
return runCapture2(["git", "-C", repoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
1302
2780
|
}
|
|
1303
2781
|
function isGreptileGithubLogin(login) {
|
|
1304
2782
|
const normalized = (login || "").toLowerCase().replace(/\[bot\]$/, "");
|
|
@@ -1438,7 +2916,7 @@ function filterActionableGithubGreptileThreads(threads) {
|
|
|
1438
2916
|
}
|
|
1439
2917
|
function resolvePrRepoRoot(projectRoot, prState) {
|
|
1440
2918
|
const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
|
|
1441
|
-
if (prState.target === "monorepo" && runtimeWorkspace &&
|
|
2919
|
+
if (prState.target === "monorepo" && runtimeWorkspace && existsSync5(resolve4(runtimeWorkspace, ".git"))) {
|
|
1442
2920
|
return runtimeWorkspace;
|
|
1443
2921
|
}
|
|
1444
2922
|
const paths = resolveHarnessPaths(projectRoot);
|
|
@@ -1449,10 +2927,10 @@ function isCommitAncestorOfPrHead(projectRoot, prState, reviewedCommit, headComm
|
|
|
1449
2927
|
return false;
|
|
1450
2928
|
}
|
|
1451
2929
|
const repoRoot = resolvePrRepoRoot(projectRoot, prState);
|
|
1452
|
-
return
|
|
2930
|
+
return runCapture2(["git", "-C", repoRoot, "merge-base", "--is-ancestor", reviewedCommit, headCommit], projectRoot).exitCode === 0;
|
|
1453
2931
|
}
|
|
1454
2932
|
function summarizeComment(input) {
|
|
1455
|
-
const text = stripHtml(input).replace(/\s+/g, " ").trim();
|
|
2933
|
+
const text = mg().stripHtml(input).replace(/\s+/g, " ").trim();
|
|
1456
2934
|
return text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
1457
2935
|
}
|
|
1458
2936
|
function asGreptileInfrastructureWarning(reason) {
|
|
@@ -1469,29 +2947,23 @@ function isAiReviewApproved(input) {
|
|
|
1469
2947
|
}
|
|
1470
2948
|
|
|
1471
2949
|
// packages/bundle-default-lifecycle/src/control-plane/completion-verification.ts
|
|
1472
|
-
import {
|
|
1473
|
-
import {
|
|
1474
|
-
import {
|
|
1475
|
-
import { readSourceAwareTaskStatus } from "@rig/runtime/control-plane/tasks/source-aware-task-config-source";
|
|
1476
|
-
import {
|
|
1477
|
-
buildTaskRunLifecycleComment,
|
|
1478
|
-
updateConfiguredTaskSourceTask
|
|
1479
|
-
} from "@rig/runtime/control-plane/tasks/source-lifecycle";
|
|
1480
|
-
import { buildPluginHostContext } from "@rig/runtime/control-plane/plugin-host-context";
|
|
2950
|
+
import { runCapture as runCapture3 } from "@rig/core/exec";
|
|
2951
|
+
import { resolveHarnessPaths as resolveHarnessPaths2 } from "@rig/core/harness-paths";
|
|
2952
|
+
import { buildPluginHostContext } from "@rig/core/plugin-host-context";
|
|
1481
2953
|
async function closeCompletedTaskSource(projectRoot, taskId) {
|
|
1482
|
-
const comment = buildTaskRunLifecycleComment({
|
|
2954
|
+
const comment = taskData().buildTaskRunLifecycleComment({
|
|
1483
2955
|
runId: process.env.RIG_SERVER_RUN_ID || taskId,
|
|
1484
2956
|
status: "closed",
|
|
1485
2957
|
summary: "Rig completion verification approved and closed this task.",
|
|
1486
|
-
runtimeWorkspace: process.env.RIG_TASK_WORKSPACE,
|
|
1487
|
-
logsDir: process.env.RIG_LOGS_DIR,
|
|
1488
|
-
sessionDir: process.env.RIG_SESSION_FILE
|
|
2958
|
+
...process.env.RIG_TASK_WORKSPACE !== undefined ? { runtimeWorkspace: process.env.RIG_TASK_WORKSPACE } : {},
|
|
2959
|
+
...process.env.RIG_LOGS_DIR !== undefined ? { logsDir: process.env.RIG_LOGS_DIR } : {},
|
|
2960
|
+
...process.env.RIG_SESSION_FILE !== undefined ? { sessionDir: process.env.RIG_SESSION_FILE } : {}
|
|
1489
2961
|
});
|
|
1490
|
-
const result = await updateConfiguredTaskSourceTask(projectRoot, {
|
|
2962
|
+
const result = await taskData().updateConfiguredTaskSourceTask(projectRoot, {
|
|
1491
2963
|
taskId,
|
|
1492
2964
|
update: { status: "closed", comment }
|
|
1493
2965
|
});
|
|
1494
|
-
const status = result.status ?? await readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
2966
|
+
const status = result.status ?? await taskData().readSourceAwareTaskStatus(projectRoot, result.taskId);
|
|
1495
2967
|
if (!result.updated && status == null) {
|
|
1496
2968
|
return {
|
|
1497
2969
|
ok: true,
|
|
@@ -1511,7 +2983,7 @@ function isClosedStatus(status) {
|
|
|
1511
2983
|
}
|
|
1512
2984
|
async function runCompletionVerificationGate(projectRoot) {
|
|
1513
2985
|
seedPolicyFromContent(resolvePolicyContent(projectRoot));
|
|
1514
|
-
const taskId = currentTaskId(projectRoot);
|
|
2986
|
+
const taskId = taskData().currentTaskId(projectRoot);
|
|
1515
2987
|
if (!taskId) {
|
|
1516
2988
|
return { ok: true };
|
|
1517
2989
|
}
|
|
@@ -1520,7 +2992,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1520
2992
|
let sourceCloseoutAllowed = false;
|
|
1521
2993
|
console.log(`=== Completion Verification: ${taskId} ===`);
|
|
1522
2994
|
const scopes = await resolveTaskScopes(projectRoot, taskId);
|
|
1523
|
-
const taskChangedFiles = changedFilesForTask(projectRoot, taskId, true);
|
|
2995
|
+
const taskChangedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
|
|
1524
2996
|
const sourceInArtifacts = taskChangedFiles.filter((file) => /^artifacts\//.test(file) && /\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file));
|
|
1525
2997
|
if (sourceInArtifacts.length > 0) {
|
|
1526
2998
|
console.log(`
|
|
@@ -1539,7 +3011,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1539
3011
|
});
|
|
1540
3012
|
console.log(`
|
|
1541
3013
|
[1/3] Task validation...`);
|
|
1542
|
-
if (!await taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
|
|
3014
|
+
if (!await taskData().taskValidate(projectRoot, taskId, pluginHostCtx?.validatorRegistry ?? undefined)) {
|
|
1543
3015
|
console.log(`FAIL: Validation failed for ${taskId}`);
|
|
1544
3016
|
failed = true;
|
|
1545
3017
|
} else {
|
|
@@ -1552,7 +3024,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1552
3024
|
failed = true;
|
|
1553
3025
|
}
|
|
1554
3026
|
}
|
|
1555
|
-
taskArtifacts(projectRoot, taskId);
|
|
3027
|
+
taskData().taskArtifacts(projectRoot, taskId);
|
|
1556
3028
|
const policy = loadPolicy(projectRoot);
|
|
1557
3029
|
const openPrEnabled = policy.completion.checks.includes("open-pr");
|
|
1558
3030
|
const autoMergeEnabled = policy.completion.checks.includes("auto-merge");
|
|
@@ -1579,7 +3051,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1579
3051
|
} else {
|
|
1580
3052
|
console.log("Verifier preflight: skipped (earlier checks failed)");
|
|
1581
3053
|
}
|
|
1582
|
-
const pendingTaskChangedFiles = pendingFilesForTask(projectRoot, taskId, true);
|
|
3054
|
+
const pendingTaskChangedFiles = taskData().pendingFilesForTask(projectRoot, taskId, true);
|
|
1583
3055
|
const hasLocalChanges = pendingTaskChangedFiles.length > 0;
|
|
1584
3056
|
console.log(`
|
|
1585
3057
|
[post] Auto-committing task changes...`);
|
|
@@ -1652,14 +3124,15 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1652
3124
|
console.log(`
|
|
1653
3125
|
[post] Auto-merge...`);
|
|
1654
3126
|
try {
|
|
1655
|
-
const prs =
|
|
3127
|
+
const prs = readPrMetadata(projectRoot, taskId);
|
|
1656
3128
|
if (prs.length === 0) {
|
|
1657
3129
|
console.log("Auto-merge: skipped (no PR metadata found)");
|
|
1658
3130
|
} else {
|
|
3131
|
+
const mergeGate = await resolvePrMergeGateService(projectRoot);
|
|
1659
3132
|
let cycle = 0;
|
|
1660
3133
|
for (const pr of prs) {
|
|
1661
3134
|
cycle += 1;
|
|
1662
|
-
const gate = await
|
|
3135
|
+
const gate = await mergeGate.runGate({
|
|
1663
3136
|
projectRoot,
|
|
1664
3137
|
prUrl: pr.url,
|
|
1665
3138
|
taskId,
|
|
@@ -1667,7 +3140,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1667
3140
|
cycle,
|
|
1668
3141
|
final: true,
|
|
1669
3142
|
command: async (args, options) => {
|
|
1670
|
-
const result =
|
|
3143
|
+
const result = runCapture3(["gh", ...args], options?.cwd ?? projectRoot);
|
|
1671
3144
|
return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
|
|
1672
3145
|
}
|
|
1673
3146
|
});
|
|
@@ -1684,7 +3157,7 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1684
3157
|
pr,
|
|
1685
3158
|
method: "squash",
|
|
1686
3159
|
deleteBranch: true,
|
|
1687
|
-
matchHeadCommit:
|
|
3160
|
+
matchHeadCommit: mergeGate.resolveHeadSha({ result: gate, prUrl: pr.url })
|
|
1688
3161
|
});
|
|
1689
3162
|
if (mergeResult.status === "merged" || mergeResult.status === "already-merged") {
|
|
1690
3163
|
console.log(`OK: PR merge confirmed (${pr.repoLabel}): ${pr.url}`);
|
|
@@ -1704,9 +3177,9 @@ async function runCompletionVerificationGate(projectRoot) {
|
|
|
1704
3177
|
console.log(`
|
|
1705
3178
|
[post] Auto-merge: skipped (not in policy completion.checks)`);
|
|
1706
3179
|
}
|
|
1707
|
-
const artifactDir =
|
|
1708
|
-
|
|
1709
|
-
|
|
3180
|
+
const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
3181
|
+
mkdirSync3(artifactDir, { recursive: true });
|
|
3182
|
+
writeFileSync3(resolve5(artifactDir, "review-status.txt"), failed ? `REJECTED
|
|
1710
3183
|
` : `APPROVED
|
|
1711
3184
|
`, "utf-8");
|
|
1712
3185
|
if (!failed) {
|
|
@@ -1756,8 +3229,8 @@ async function runBunTool(args, cwd) {
|
|
|
1756
3229
|
};
|
|
1757
3230
|
}
|
|
1758
3231
|
async function runProtoQualityGate(monorepoRoot) {
|
|
1759
|
-
const protosDir =
|
|
1760
|
-
if (!
|
|
3232
|
+
const protosDir = resolve5(monorepoRoot, "packages", "protos");
|
|
3233
|
+
if (!existsSync6(protosDir)) {
|
|
1761
3234
|
console.log(`FAIL: Proto workspace not found at ${protosDir}`);
|
|
1762
3235
|
return false;
|
|
1763
3236
|
}
|
|
@@ -1784,7 +3257,7 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1784
3257
|
console.log(generate.stderr || generate.stdout);
|
|
1785
3258
|
ok = false;
|
|
1786
3259
|
} else {
|
|
1787
|
-
const drift =
|
|
3260
|
+
const drift = runCapture3(["git", "-C", protosDir, "status", "--porcelain", "--", "gen/ts"], monorepoRoot);
|
|
1788
3261
|
if (drift.exitCode !== 0) {
|
|
1789
3262
|
console.log("FAIL: Could not inspect generated proto drift");
|
|
1790
3263
|
console.log(drift.stderr || drift.stdout);
|
|
@@ -1805,12 +3278,12 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1805
3278
|
} else {
|
|
1806
3279
|
console.log("OK: Generated TypeScript compiles");
|
|
1807
3280
|
}
|
|
1808
|
-
const workflowPath =
|
|
1809
|
-
if (!
|
|
3281
|
+
const workflowPath = resolve5(monorepoRoot, ".github", "workflows", "pull-request-gate.yml");
|
|
3282
|
+
if (!existsSync6(workflowPath)) {
|
|
1810
3283
|
console.log(`FAIL: Missing workflow gate file at ${workflowPath}`);
|
|
1811
3284
|
ok = false;
|
|
1812
3285
|
} else {
|
|
1813
|
-
const workflow =
|
|
3286
|
+
const workflow = readFileSync4(workflowPath, "utf-8");
|
|
1814
3287
|
if (workflow.includes("if: false && needs.detect.outputs.protos_changed == 'true'")) {
|
|
1815
3288
|
console.log("FAIL: Proto quality CI gate is disabled in pull-request-gate.yml");
|
|
1816
3289
|
ok = false;
|
|
@@ -1821,13 +3294,13 @@ async function runProtoQualityGate(monorepoRoot) {
|
|
|
1821
3294
|
return ok;
|
|
1822
3295
|
}
|
|
1823
3296
|
function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
1824
|
-
const unpushed =
|
|
3297
|
+
const unpushed = runCapture3(["git", "-C", repoRoot, "log", "@{u}..HEAD", "--oneline"], projectRoot);
|
|
1825
3298
|
if (unpushed.exitCode === 0 && unpushed.stdout.trim().length > 0)
|
|
1826
3299
|
return true;
|
|
1827
3300
|
if (unpushed.exitCode !== 0) {
|
|
1828
|
-
const branch =
|
|
3301
|
+
const branch = runCapture3(["git", "-C", repoRoot, "rev-parse", "--abbrev-ref", "HEAD"], projectRoot);
|
|
1829
3302
|
if (branch.exitCode === 0 && branch.stdout.trim()) {
|
|
1830
|
-
const remote =
|
|
3303
|
+
const remote = runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branch.stdout.trim()}`], projectRoot);
|
|
1831
3304
|
if (remote.exitCode !== 0)
|
|
1832
3305
|
return true;
|
|
1833
3306
|
}
|
|
@@ -1836,10 +3309,10 @@ function repoHasRemoteRelevantCommits(projectRoot, repoRoot) {
|
|
|
1836
3309
|
}
|
|
1837
3310
|
function repoHasPublishedTaskBranch(projectRoot, repoRoot, taskId) {
|
|
1838
3311
|
const branchRef = resolveTaskBranchRef(projectRoot, taskId);
|
|
1839
|
-
return
|
|
3312
|
+
return runCapture3(["git", "-C", repoRoot, "ls-remote", "--exit-code", "origin", `refs/heads/${branchRef}`], projectRoot).exitCode === 0;
|
|
1840
3313
|
}
|
|
1841
3314
|
async function readJsonFileIfPresent(path) {
|
|
1842
|
-
if (!
|
|
3315
|
+
if (!existsSync6(path)) {
|
|
1843
3316
|
return null;
|
|
1844
3317
|
}
|
|
1845
3318
|
try {
|
|
@@ -1850,9 +3323,9 @@ async function readJsonFileIfPresent(path) {
|
|
|
1850
3323
|
}
|
|
1851
3324
|
async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
1852
3325
|
const failedApproachesPath = paths.failedApproachesPath;
|
|
1853
|
-
const artifactDir =
|
|
1854
|
-
const reviewStatePath =
|
|
1855
|
-
const reviewFeedbackPath =
|
|
3326
|
+
const artifactDir = resolve5(paths.artifactsDir, safePathSegment2(taskId, { fallback: "task", maxLength: 96 }));
|
|
3327
|
+
const reviewStatePath = resolve5(artifactDir, "review-state.json");
|
|
3328
|
+
const reviewFeedbackPath = resolve5(artifactDir, "review-feedback.md");
|
|
1856
3329
|
let summary = "Verifier rejected completion. Read review-feedback.md for required fixes.";
|
|
1857
3330
|
const parsedReviewState = await readJsonFileIfPresent(reviewStatePath);
|
|
1858
3331
|
if (parsedReviewState) {
|
|
@@ -1862,12 +3335,12 @@ async function recordVerifierFailure(projectRoot, taskId, paths) {
|
|
|
1862
3335
|
}
|
|
1863
3336
|
}
|
|
1864
3337
|
let attempts = 1;
|
|
1865
|
-
if (
|
|
1866
|
-
const content =
|
|
1867
|
-
attempts = (content.match(new RegExp(`^## ${
|
|
3338
|
+
if (existsSync6(failedApproachesPath)) {
|
|
3339
|
+
const content = readFileSync4(failedApproachesPath, "utf-8");
|
|
3340
|
+
attempts = (content.match(new RegExp(`^## ${escapeRegExp2(taskId)}\\b`, "gm")) || []).length + 1;
|
|
1868
3341
|
} else {
|
|
1869
|
-
|
|
1870
|
-
|
|
3342
|
+
mkdirSync3(resolve5(failedApproachesPath, ".."), { recursive: true });
|
|
3343
|
+
writeFileSync3(failedApproachesPath, `# Failed Approaches
|
|
1871
3344
|
|
|
1872
3345
|
`, "utf-8");
|
|
1873
3346
|
}
|
|
@@ -1891,7 +3364,7 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
1891
3364
|
statePaths.add(resolveHarnessPaths2(hostProjectRoot).taskRepoCommitsPath);
|
|
1892
3365
|
}
|
|
1893
3366
|
const repos = {};
|
|
1894
|
-
const monoHead =
|
|
3367
|
+
const monoHead = runCapture3(["git", "-C", paths.monorepoRoot, "rev-parse", "HEAD"], projectRoot).stdout.trim();
|
|
1895
3368
|
if (monoHead) {
|
|
1896
3369
|
repos["monorepo"] = monoHead;
|
|
1897
3370
|
}
|
|
@@ -1905,8 +3378,8 @@ async function recordTaskRepoCommits(projectRoot, taskId, paths) {
|
|
|
1905
3378
|
recorded_at: new Date().toISOString(),
|
|
1906
3379
|
repos
|
|
1907
3380
|
};
|
|
1908
|
-
|
|
1909
|
-
|
|
3381
|
+
mkdirSync3(resolve5(statePath, ".."), { recursive: true });
|
|
3382
|
+
writeFileSync3(statePath, `${JSON.stringify(state, null, 2)}
|
|
1910
3383
|
`, "utf-8");
|
|
1911
3384
|
}
|
|
1912
3385
|
}
|