@h-rig/cli-surface-plugin 0.0.6-alpha.156 → 0.0.6-alpha.158

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.
Files changed (112) hide show
  1. package/dist/src/app/drone-ui.d.ts +0 -11
  2. package/dist/src/app/drone-ui.js +0 -114
  3. package/dist/src/commands/_async-ui.d.ts +1 -1
  4. package/dist/src/commands/_cli-format.d.ts +0 -29
  5. package/dist/src/commands/_cli-format.js +59 -113
  6. package/dist/src/commands/_connection-state.d.ts +6 -33
  7. package/dist/src/commands/_connection-state.js +654 -138
  8. package/dist/src/commands/_doctor-checks.d.ts +2 -5
  9. package/dist/src/commands/_doctor-checks.js +10 -9
  10. package/dist/src/commands/_help-catalog.d.ts +2 -1
  11. package/dist/src/commands/_help-catalog.js +654 -7
  12. package/dist/src/commands/_inprocess-services.d.ts +5 -5
  13. package/dist/src/commands/_inprocess-services.js +1 -1
  14. package/dist/src/commands/_parsers.js +651 -12
  15. package/dist/src/commands/_paths.d.ts +0 -2
  16. package/dist/src/commands/_paths.js +2 -10
  17. package/dist/src/commands/_pi-install.d.ts +2 -12
  18. package/dist/src/commands/_pi-install.js +3 -36
  19. package/dist/src/commands/_policy.js +659 -20
  20. package/dist/src/commands/agent.d.ts +1 -1
  21. package/dist/src/commands/agent.js +675 -24
  22. package/dist/src/commands/config.d.ts +1 -1
  23. package/dist/src/commands/config.js +656 -21
  24. package/dist/src/commands/dist.d.ts +1 -1
  25. package/dist/src/commands/dist.js +828 -102
  26. package/dist/src/commands/doctor.d.ts +1 -1
  27. package/dist/src/commands/doctor.js +658 -12
  28. package/dist/src/commands/github.d.ts +1 -1
  29. package/dist/src/commands/github.js +658 -19
  30. package/dist/src/commands/inbox.d.ts +12 -8
  31. package/dist/src/commands/inbox.js +741 -22
  32. package/dist/src/commands/init.d.ts +17 -19
  33. package/dist/src/commands/init.js +836 -306
  34. package/dist/src/commands/inspect.d.ts +5 -6
  35. package/dist/src/commands/inspect.js +754 -42
  36. package/dist/src/commands/pi.d.ts +1 -1
  37. package/dist/src/commands/pi.js +655 -16
  38. package/dist/src/commands/plugin.d.ts +9 -9
  39. package/dist/src/commands/plugin.js +652 -13
  40. package/dist/src/commands/profile-and-review.d.ts +1 -1
  41. package/dist/src/commands/profile-and-review.js +655 -16
  42. package/dist/src/commands/queue.d.ts +1 -1
  43. package/dist/src/commands/queue.js +871 -12
  44. package/dist/src/commands/remote-client.d.ts +152 -0
  45. package/dist/src/commands/remote-client.js +475 -0
  46. package/dist/src/commands/remote.d.ts +1 -1
  47. package/dist/src/commands/remote.js +1100 -29
  48. package/dist/src/commands/repo-git-harness.d.ts +1 -1
  49. package/dist/src/commands/repo-git-harness.js +2321 -47
  50. package/dist/src/commands/run.d.ts +10 -6
  51. package/dist/src/commands/run.js +830 -50
  52. package/dist/src/commands/server.d.ts +1 -1
  53. package/dist/src/commands/server.js +649 -11
  54. package/dist/src/commands/setup.d.ts +2 -2
  55. package/dist/src/commands/setup.js +829 -18
  56. package/dist/src/commands/stats.d.ts +2 -4
  57. package/dist/src/commands/stats.js +1299 -20
  58. package/dist/src/commands/test.d.ts +1 -1
  59. package/dist/src/commands/test.js +648 -9
  60. package/dist/src/commands/triage.d.ts +2 -3
  61. package/dist/src/commands/triage.js +657 -11
  62. package/dist/src/commands/workspace.d.ts +1 -1
  63. package/dist/src/commands/workspace.js +1280 -15
  64. package/dist/src/control-plane/agent-binary-build.d.ts +9 -0
  65. package/dist/src/control-plane/agent-binary-build.js +88 -0
  66. package/dist/src/control-plane/embedded-native-assets.d.ts +7 -0
  67. package/dist/src/control-plane/embedded-native-assets.js +6 -0
  68. package/dist/src/control-plane/guard.d.ts +17 -0
  69. package/dist/src/control-plane/guard.js +684 -0
  70. package/dist/src/control-plane/harness-cli.d.ts +12 -0
  71. package/dist/src/control-plane/harness-cli.js +1623 -0
  72. package/dist/src/control-plane/native/git-ops.d.ts +67 -0
  73. package/dist/src/control-plane/native/git-ops.js +1381 -0
  74. package/dist/src/control-plane/native/github-auth-env.d.ts +1 -0
  75. package/dist/src/control-plane/native/github-auth-env.js +21 -0
  76. package/dist/src/control-plane/native/host-git.d.ts +4 -0
  77. package/dist/src/control-plane/native/host-git.js +51 -0
  78. package/dist/src/control-plane/priority-queue.d.ts +22 -0
  79. package/dist/src/control-plane/priority-queue.js +212 -0
  80. package/dist/src/control-plane/rigfig.d.ts +9 -0
  81. package/dist/src/control-plane/rigfig.js +70 -0
  82. package/dist/src/control-plane/scope.d.ts +3 -0
  83. package/dist/src/control-plane/scope.js +58 -0
  84. package/dist/src/control-plane/setup-status.d.ts +44 -0
  85. package/dist/src/control-plane/setup-status.js +164 -0
  86. package/dist/src/control-plane/task-data.d.ts +2 -0
  87. package/dist/src/control-plane/task-data.js +12 -0
  88. package/dist/src/control-plane/workspace-ops.d.ts +79 -0
  89. package/dist/src/control-plane/workspace-ops.js +639 -0
  90. package/dist/src/help-catalog-data.d.ts +7 -0
  91. package/dist/src/help-catalog-data.js +660 -0
  92. package/dist/src/kernel-dispatch.js +1 -3
  93. package/dist/src/plugin.js +10072 -30
  94. package/dist/src/runner.d.ts +7 -9
  95. package/dist/src/runner.js +750 -30
  96. package/package.json +12 -13
  97. package/dist/src/commands/_json-output.d.ts +0 -11
  98. package/dist/src/commands/_json-output.js +0 -54
  99. package/dist/src/commands/_pi-frontend.d.ts +0 -35
  100. package/dist/src/commands/_pi-frontend.js +0 -64
  101. package/dist/src/commands/_run-driver-helpers.d.ts +0 -26
  102. package/dist/src/commands/_run-driver-helpers.js +0 -132
  103. package/dist/src/commands/task-run-driver.d.ts +0 -93
  104. package/dist/src/commands/task-run-driver.js +0 -136
  105. package/dist/src/commands/task.d.ts +0 -46
  106. package/dist/src/commands/task.js +0 -555
  107. package/dist/src/provider-model.d.ts +0 -34
  108. package/dist/src/provider-model.js +0 -56
  109. package/dist/src/rig-config-package-deps.d.ts +0 -10
  110. package/dist/src/rig-config-package-deps.js +0 -272
  111. package/dist/src/version.d.ts +0 -8
  112. package/dist/src/version.js +0 -47
@@ -1,46 +1,2282 @@
1
1
  // @bun
2
2
  // packages/cli-surface-plugin/src/runner.ts
3
- import { EventBus } from "@rig/runtime/control-plane/runtime/events";
4
- import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
5
- import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
6
- import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
3
+ import { EventBus } from "@rig/core/runtime-events";
4
+ import { CliError as RuntimeCliError } from "@rig/contracts";
7
5
 
6
+ // packages/cli-surface-plugin/src/control-plane/guard.ts
7
+ import { optimizeNextInvocation } from "bun:jsc";
8
+ import { existsSync, readFileSync, statSync } from "fs";
9
+ import { resolve } from "path";
10
+
11
+ // packages/cli-surface-plugin/src/control-plane/scope.ts
12
+ import { getScopeRules } from "@rig/core/scope-rules";
13
+ var scopeRegexCache = new Map;
14
+ function unique(values) {
15
+ return [...new Set(values)];
16
+ }
17
+ function normalizeRelativeScopePath(inputPath) {
18
+ let normalized = inputPath.replace(/^\.\//, "");
19
+ const rules = getScopeRules();
20
+ if (rules?.stripPrefixes) {
21
+ for (const prefix of rules.stripPrefixes) {
22
+ if (normalized.startsWith(prefix)) {
23
+ normalized = normalized.slice(prefix.length);
24
+ }
25
+ }
26
+ }
27
+ return normalized;
28
+ }
29
+ function normalizePathToScope(projectRoot, monorepoRoot, inputPath) {
30
+ let normalized = inputPath.replace(/^\.\//, "");
31
+ if (normalized.startsWith(projectRoot + "/")) {
32
+ normalized = normalized.slice(projectRoot.length + 1);
33
+ }
34
+ if (normalized.startsWith(monorepoRoot + "/")) {
35
+ normalized = normalized.slice(monorepoRoot.length + 1);
36
+ }
37
+ return normalizeRelativeScopePath(normalized);
38
+ }
39
+ function scopeGlobToRegex(glob) {
40
+ const cached = scopeRegexCache.get(glob);
41
+ if (cached) {
42
+ return cached;
43
+ }
44
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "__GLOBSTAR__").replace(/\*/g, "[^/]*").replace(/__GLOBSTAR__/g, ".*");
45
+ const compiled = new RegExp(`^${escaped}$`);
46
+ scopeRegexCache.set(glob, compiled);
47
+ return compiled;
48
+ }
49
+ function scopeMatches(path, scopes) {
50
+ const pathVariants = unique([path, normalizeRelativeScopePath(path)]);
51
+ for (const scope of scopes) {
52
+ const scopeVariants = unique([scope, normalizeRelativeScopePath(scope)]);
53
+ for (const candidatePath of pathVariants) {
54
+ for (const candidateScope of scopeVariants) {
55
+ if (candidatePath === candidateScope || scopeGlobToRegex(candidateScope).test(candidatePath)) {
56
+ return true;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ return false;
62
+ }
63
+
64
+ // packages/cli-surface-plugin/src/control-plane/guard.ts
65
+ import {
66
+ POLICY_VERSION
67
+ } from "@rig/contracts";
68
+ var DEFAULT_SCOPE = {
69
+ fail_closed: true,
70
+ harness_paths_exempt: true,
71
+ runtime_paths_exempt: true
72
+ };
73
+ var DEFAULT_SANDBOX = {
74
+ mode: "enforce",
75
+ network: true,
76
+ read_deny: [],
77
+ write_allow_from_runtime: true
78
+ };
79
+ var DEFAULT_ISOLATION = {
80
+ default_mode: "worktree",
81
+ repo_symlink_fallback: false,
82
+ strict_provisioning: true,
83
+ fail_closed_on_provision_error: true
84
+ };
85
+ var DEFAULT_COMPLETION = {
86
+ derive_checks_from_scope: true,
87
+ checks: [],
88
+ typescript_config_probe: ["tsconfig.json"],
89
+ eslint_config_probe: [".eslintrc.js", ".eslintrc.json", "eslint.config.js"]
90
+ };
91
+ var DEFAULT_RUNTIME_IMAGE = {
92
+ deps: {
93
+ monorepo_install: false,
94
+ hp_next_install: false
95
+ },
96
+ plugins_require_binaries: true
97
+ };
98
+ var DEFAULT_RUNTIME_SNAPSHOT = {
99
+ enabled: true
100
+ };
101
+ function defaultPolicy() {
102
+ return {
103
+ version: POLICY_VERSION,
104
+ mode: "enforce",
105
+ scope: { ...DEFAULT_SCOPE },
106
+ rules: [],
107
+ sandbox: { ...DEFAULT_SANDBOX },
108
+ isolation: { ...DEFAULT_ISOLATION },
109
+ completion: { ...DEFAULT_COMPLETION },
110
+ runtime_image: {
111
+ deps: { ...DEFAULT_RUNTIME_IMAGE.deps },
112
+ plugins_require_binaries: DEFAULT_RUNTIME_IMAGE.plugins_require_binaries
113
+ },
114
+ runtime_snapshot: { ...DEFAULT_RUNTIME_SNAPSHOT }
115
+ };
116
+ }
117
+ var policyCache = null;
118
+ var policyCachePath = null;
119
+ var seededPolicyConfig = null;
120
+ var compiledRegexCache = new Map;
121
+ function loadPolicy(projectRoot) {
122
+ if (seededPolicyConfig) {
123
+ return seededPolicyConfig;
124
+ }
125
+ const configPath = resolve(projectRoot, "rig/policy/policy.json");
126
+ if (!existsSync(configPath)) {
127
+ return defaultPolicy();
128
+ }
129
+ let mtimeMs;
130
+ try {
131
+ mtimeMs = statSync(configPath).mtimeMs;
132
+ } catch {
133
+ return defaultPolicy();
134
+ }
135
+ if (policyCache && policyCachePath === configPath && policyCache.mtimeMs === mtimeMs) {
136
+ return policyCache.config;
137
+ }
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(readFileSync(configPath, "utf-8"));
141
+ } catch {
142
+ return defaultPolicy();
143
+ }
144
+ const config = mergeWithDefaults(parsed);
145
+ policyCache = { mtimeMs, config };
146
+ policyCachePath = configPath;
147
+ return config;
148
+ }
149
+ function mergeWithDefaults(parsed) {
150
+ const base = defaultPolicy();
151
+ if (typeof parsed.mode === "string" && isValidMode(parsed.mode)) {
152
+ base.mode = parsed.mode;
153
+ }
154
+ if (parsed.scope && typeof parsed.scope === "object" && !Array.isArray(parsed.scope)) {
155
+ const s = parsed.scope;
156
+ if (typeof s.fail_closed === "boolean")
157
+ base.scope.fail_closed = s.fail_closed;
158
+ if (typeof s.harness_paths_exempt === "boolean")
159
+ base.scope.harness_paths_exempt = s.harness_paths_exempt;
160
+ if (typeof s.runtime_paths_exempt === "boolean")
161
+ base.scope.runtime_paths_exempt = s.runtime_paths_exempt;
162
+ }
163
+ if (Array.isArray(parsed.rules)) {
164
+ base.rules = precompilePolicyRuleRegexes(parsed.rules.filter(isValidRule));
165
+ }
166
+ if (Array.isArray(parsed.deny) && base.rules.length === 0) {
167
+ base.rules = precompilePolicyRuleRegexes(migrateLegacyDeny(parsed.deny));
168
+ }
169
+ if (parsed.sandbox && typeof parsed.sandbox === "object" && !Array.isArray(parsed.sandbox)) {
170
+ const sb = parsed.sandbox;
171
+ if (typeof sb.mode === "string" && isValidMode(sb.mode))
172
+ base.sandbox.mode = sb.mode;
173
+ if (typeof sb.network === "boolean")
174
+ base.sandbox.network = sb.network;
175
+ if (Array.isArray(sb.read_deny))
176
+ base.sandbox.read_deny = sb.read_deny.filter((v) => typeof v === "string");
177
+ if (typeof sb.write_allow_from_runtime === "boolean")
178
+ base.sandbox.write_allow_from_runtime = sb.write_allow_from_runtime;
179
+ }
180
+ if (parsed.isolation && typeof parsed.isolation === "object" && !Array.isArray(parsed.isolation)) {
181
+ const iso = parsed.isolation;
182
+ if (iso.default_mode === "worktree")
183
+ base.isolation.default_mode = iso.default_mode;
184
+ if (typeof iso.repo_symlink_fallback === "boolean")
185
+ base.isolation.repo_symlink_fallback = iso.repo_symlink_fallback;
186
+ if (typeof iso.strict_provisioning === "boolean")
187
+ base.isolation.strict_provisioning = iso.strict_provisioning;
188
+ if (typeof iso.fail_closed_on_provision_error === "boolean")
189
+ base.isolation.fail_closed_on_provision_error = iso.fail_closed_on_provision_error;
190
+ }
191
+ if (parsed.completion && typeof parsed.completion === "object" && !Array.isArray(parsed.completion)) {
192
+ const comp = parsed.completion;
193
+ if (typeof comp.derive_checks_from_scope === "boolean")
194
+ base.completion.derive_checks_from_scope = comp.derive_checks_from_scope;
195
+ if (Array.isArray(comp.checks))
196
+ base.completion.checks = comp.checks.filter((v) => typeof v === "string");
197
+ if (Array.isArray(comp.typescript_config_probe))
198
+ base.completion.typescript_config_probe = comp.typescript_config_probe.filter((v) => typeof v === "string");
199
+ if (Array.isArray(comp.eslint_config_probe))
200
+ base.completion.eslint_config_probe = comp.eslint_config_probe.filter((v) => typeof v === "string");
201
+ }
202
+ if (parsed.runtime_image && typeof parsed.runtime_image === "object" && !Array.isArray(parsed.runtime_image)) {
203
+ const runtimeImage = parsed.runtime_image;
204
+ if (runtimeImage.deps && typeof runtimeImage.deps === "object" && !Array.isArray(runtimeImage.deps)) {
205
+ const deps = runtimeImage.deps;
206
+ if (typeof deps.monorepo_install === "boolean") {
207
+ base.runtime_image.deps.monorepo_install = deps.monorepo_install;
208
+ }
209
+ if (typeof deps.hp_next_install === "boolean") {
210
+ base.runtime_image.deps.hp_next_install = deps.hp_next_install;
211
+ }
212
+ }
213
+ if (typeof runtimeImage.plugins_require_binaries === "boolean") {
214
+ base.runtime_image.plugins_require_binaries = runtimeImage.plugins_require_binaries;
215
+ }
216
+ }
217
+ if (parsed.runtime_snapshot && typeof parsed.runtime_snapshot === "object" && !Array.isArray(parsed.runtime_snapshot)) {
218
+ const runtimeSnapshot = parsed.runtime_snapshot;
219
+ if (typeof runtimeSnapshot.enabled === "boolean") {
220
+ base.runtime_snapshot.enabled = runtimeSnapshot.enabled;
221
+ }
222
+ }
223
+ return base;
224
+ }
225
+ function isValidMode(value) {
226
+ return value === "off" || value === "observe" || value === "enforce";
227
+ }
228
+ function isValidRule(value) {
229
+ if (!value || typeof value !== "object" || Array.isArray(value))
230
+ return false;
231
+ const r = value;
232
+ return typeof r.id === "string" && typeof r.category === "string" && r.match != null && typeof r.match === "object";
233
+ }
234
+ function migrateLegacyDeny(deny) {
235
+ const rules = [];
236
+ for (const entry of deny) {
237
+ if (typeof entry.id !== "string")
238
+ continue;
239
+ const match = {};
240
+ if (typeof entry.pattern === "string")
241
+ match.pattern = entry.pattern;
242
+ if (typeof entry.regex === "string")
243
+ match.regex = entry.regex;
244
+ if (!match.pattern && !match.regex)
245
+ continue;
246
+ rules.push({
247
+ id: entry.id,
248
+ category: "command",
249
+ match,
250
+ action: "block",
251
+ ...typeof entry.description === "string" ? { description: entry.description } : {}
252
+ });
253
+ }
254
+ return rules;
255
+ }
256
+ function precompilePolicyRuleRegexes(rules) {
257
+ return rules.map((rule) => {
258
+ const compiledRegex = rule.match.regex ? compileSafeRegex(rule.match.regex, `rules.${rule.id}.match.regex`, true) : undefined;
259
+ const compiledUnlessRegex = rule.unless?.regex ? compileSafeRegex(rule.unless.regex, `rules.${rule.id}.unless.regex`, true) : undefined;
260
+ return {
261
+ ...rule,
262
+ ...compiledRegex ? { compiledRegex } : {},
263
+ ...compiledUnlessRegex ? { compiledUnlessRegex } : {}
264
+ };
265
+ });
266
+ }
267
+ function getRegexUnsafeReason(pattern) {
268
+ if (pattern.length > 512) {
269
+ return "pattern exceeds max safe length (512 chars)";
270
+ }
271
+ if (/\\[1-9]/.test(pattern)) {
272
+ return "pattern uses backreferences";
273
+ }
274
+ if (/\((?:[^()\\]|\\.)*[+*](?:[^()\\]|\\.)*\)\s*[*+{]/.test(pattern)) {
275
+ return "pattern contains nested quantifiers";
276
+ }
277
+ if (/\((?:[^()\\]|\\.)*\.\\?[+*](?:[^()\\]|\\.)*\)\s*[*+{]/.test(pattern)) {
278
+ return "pattern contains nested broad quantifiers";
279
+ }
280
+ return null;
281
+ }
282
+ function compileSafeRegex(pattern, sourceLabel, logOnFailure) {
283
+ const cached = compiledRegexCache.get(pattern);
284
+ if (cached !== undefined) {
285
+ return cached ?? undefined;
286
+ }
287
+ const unsafeReason = getRegexUnsafeReason(pattern);
288
+ if (unsafeReason) {
289
+ if (logOnFailure) {
290
+ console.warn(`[policy] Skipping unsafe regex in ${sourceLabel}: ${unsafeReason}`);
291
+ }
292
+ compiledRegexCache.set(pattern, null);
293
+ return;
294
+ }
295
+ try {
296
+ const compiled = new RegExp(pattern);
297
+ compiledRegexCache.set(pattern, compiled);
298
+ return compiled;
299
+ } catch (error) {
300
+ if (logOnFailure) {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ console.warn(`[policy] Skipping invalid regex in ${sourceLabel}: ${message}`);
303
+ }
304
+ compiledRegexCache.set(pattern, null);
305
+ return;
306
+ }
307
+ }
308
+ function matchRule(rule, input) {
309
+ const { match } = rule;
310
+ if (match.pattern && input.includes(match.pattern)) {
311
+ return true;
312
+ }
313
+ if (match.regex) {
314
+ const compiled = rule.compiledRegex || compileSafeRegex(match.regex, `rules.${rule.id}.match.regex`, false);
315
+ if (!compiled) {
316
+ return false;
317
+ }
318
+ try {
319
+ return compiled.test(input);
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+ function matchRuleUnless(rule, command, taskId) {
327
+ if (!rule.unless)
328
+ return false;
329
+ if (rule.unless.regex) {
330
+ const compiled = rule.compiledUnlessRegex || compileSafeRegex(rule.unless.regex, `rules.${rule.id}.unless.regex`, false);
331
+ if (!compiled) {
332
+ return false;
333
+ }
334
+ try {
335
+ if (compiled.test(command))
336
+ return true;
337
+ } catch {}
338
+ }
339
+ if (rule.unless.task_in && taskId) {
340
+ if (rule.unless.task_in.includes(taskId))
341
+ return true;
342
+ }
343
+ return false;
344
+ }
345
+ function resolveAction(mode, matched) {
346
+ if (matched.length === 0)
347
+ return "allow";
348
+ if (mode === "off")
349
+ return "allow";
350
+ if (mode === "observe")
351
+ return "warn";
352
+ return "block";
353
+ }
354
+ function resolveAbsolutePath(projectRoot, rawPath) {
355
+ if (rawPath.startsWith("/"))
356
+ return resolve(rawPath);
357
+ return resolve(projectRoot, rawPath);
358
+ }
359
+ function isHarnessPath(projectRoot, rawPath) {
360
+ const absPath = resolveAbsolutePath(projectRoot, rawPath);
361
+ const managedRoots = [
362
+ resolve(projectRoot, "rig"),
363
+ resolve(projectRoot, ".rig"),
364
+ resolve(projectRoot, "artifacts")
365
+ ];
366
+ return managedRoots.some((root) => absPath === root || absPath.startsWith(root + "/"));
367
+ }
368
+ function isRuntimePath(projectRoot, rawPath, taskWorkspace) {
369
+ const absPath = resolveAbsolutePath(projectRoot, rawPath);
370
+ if (taskWorkspace) {
371
+ const workspaceRigRoot = resolve(taskWorkspace, ".rig");
372
+ const workspaceArtifactsRoot = resolve(taskWorkspace, "artifacts");
373
+ if (absPath === workspaceRigRoot || absPath.startsWith(workspaceRigRoot + "/") || absPath === workspaceArtifactsRoot || absPath.startsWith(workspaceArtifactsRoot + "/")) {
374
+ return true;
375
+ }
376
+ }
377
+ const runtimeRoot = resolve(projectRoot, ".rig/runtime/agents");
378
+ return absPath === runtimeRoot || absPath.startsWith(runtimeRoot + "/");
379
+ }
380
+ function isTestFile(path) {
381
+ return /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(path) || /\/(__tests__|tests|test)\//.test(path);
382
+ }
383
+ function evaluate(context) {
384
+ const policy = loadPolicy(context.projectRoot);
385
+ switch (context.evaluation.type) {
386
+ case "tool-call":
387
+ return evaluateToolCall(policy, context);
388
+ case "command":
389
+ return evaluateCommand(policy, context);
390
+ case "content-write":
391
+ return evaluateContent(policy, context);
392
+ case "file-access":
393
+ return evaluateScope(policy, context, context.evaluation.file_path, context.evaluation.access);
394
+ }
395
+ }
396
+ function evaluateScope(policy, context, filePath, access) {
397
+ const allowed = () => ({
398
+ allowed: true,
399
+ matchedRules: [],
400
+ action: "allow",
401
+ failClosed: false
402
+ });
403
+ if (policy.scope.harness_paths_exempt && isHarnessPath(context.projectRoot, filePath)) {
404
+ return allowed();
405
+ }
406
+ if (policy.scope.runtime_paths_exempt && isRuntimePath(context.projectRoot, filePath, context.taskWorkspace)) {
407
+ return allowed();
408
+ }
409
+ if (!context.taskId) {
410
+ if (access === "write" && policy.scope.fail_closed) {
411
+ return {
412
+ allowed: false,
413
+ matchedRules: [],
414
+ action: resolveAction(policy.mode, [{ id: "scope:no-task", category: "command", reason: "No active task; fail-closed for write operations" }]),
415
+ failClosed: true
416
+ };
417
+ }
418
+ return allowed();
419
+ }
420
+ const scopes = context.taskScopes || [];
421
+ if (scopes.length === 0) {
422
+ return allowed();
423
+ }
424
+ if (context.taskWorkspace && context.taskWorkspace !== context.projectRoot && filePath.startsWith("/")) {
425
+ const absPath = resolve(filePath);
426
+ if (!absPath.startsWith(context.taskWorkspace + "/") && !isHarnessPath(context.projectRoot, filePath)) {
427
+ const reason2 = `Absolute path '${filePath}' is outside task runtime boundary. Allowed root: ${context.taskWorkspace}`;
428
+ const matched2 = [{ id: "scope:workspace-boundary", category: "command", reason: reason2 }];
429
+ return {
430
+ allowed: policy.mode !== "enforce",
431
+ matchedRules: matched2,
432
+ action: resolveAction(policy.mode, matched2),
433
+ failClosed: false
434
+ };
435
+ }
436
+ }
437
+ const monorepoRoot = context.monorepoRoot || process.env.MONOREPO_ROOT?.trim() || context.taskWorkspace || context.projectRoot;
438
+ let normalizedPath = filePath;
439
+ if (context.taskWorkspace && context.taskWorkspace !== context.projectRoot && filePath.startsWith(context.taskWorkspace + "/")) {
440
+ normalizedPath = filePath.slice(context.taskWorkspace.length + 1);
441
+ }
442
+ normalizedPath = normalizePathToScope(context.projectRoot, monorepoRoot, normalizedPath);
443
+ if (scopeMatches(filePath, scopes) || scopeMatches(normalizedPath, scopes)) {
444
+ return allowed();
445
+ }
446
+ const reason = `File '${filePath}' (normalized: '${normalizedPath}') is outside scope of task ${context.taskId}`;
447
+ const matched = [{ id: "scope:out-of-scope", category: "command", reason }];
448
+ return {
449
+ allowed: policy.mode !== "enforce",
450
+ matchedRules: matched,
451
+ action: resolveAction(policy.mode, matched),
452
+ failClosed: false
453
+ };
454
+ }
455
+ function evaluateCommand(policy, context) {
456
+ const evaluation = context.evaluation;
457
+ if (evaluation.type !== "command") {
458
+ return { allowed: true, matchedRules: [], action: "allow", failClosed: false };
459
+ }
460
+ const command = evaluation.command;
461
+ const matchedRules = [];
462
+ for (const rule of policy.rules) {
463
+ if (rule.category !== "command")
464
+ continue;
465
+ if (!matchRule(rule, command))
466
+ continue;
467
+ if (matchRuleUnless(rule, command, context.taskId))
468
+ continue;
469
+ matchedRules.push({
470
+ id: rule.id,
471
+ category: rule.category,
472
+ ...rule.description !== undefined ? { description: rule.description } : {},
473
+ reason: rule.description || `Matched rule ${rule.id}`
474
+ });
475
+ }
476
+ const writeTarget = extractWriteTarget(command);
477
+ if (writeTarget && !/^\/dev\//.test(writeTarget) && !/^\/proc\//.test(writeTarget)) {
478
+ const scopeResult = evaluateScope(policy, context, writeTarget, "write");
479
+ if (!scopeResult.allowed || scopeResult.matchedRules.length > 0) {
480
+ matchedRules.push(...scopeResult.matchedRules);
481
+ }
482
+ }
483
+ const action = resolveAction(policy.mode, matchedRules);
484
+ return {
485
+ allowed: action !== "block",
486
+ matchedRules,
487
+ action,
488
+ failClosed: false
489
+ };
490
+ }
491
+ function extractWriteTarget(command) {
492
+ const redirect = command.match(/>>?\s+([^\s;|&]+)/);
493
+ if (redirect?.[1])
494
+ return redirect[1];
495
+ const tee = command.match(/tee\s+(-a\s+)?([^\s;|&]+)/);
496
+ if (tee?.[2])
497
+ return tee[2];
498
+ return "";
499
+ }
500
+ function evaluateContent(policy, context) {
501
+ const evaluation = context.evaluation;
502
+ if (evaluation.type !== "content-write") {
503
+ return { allowed: true, matchedRules: [], action: "allow", failClosed: false };
504
+ }
505
+ const { content, file_path } = evaluation;
506
+ const matchedRules = [];
507
+ const scopeResult = evaluateScope(policy, context, file_path, "write");
508
+ if (scopeResult.matchedRules.length > 0) {
509
+ matchedRules.push(...scopeResult.matchedRules);
510
+ }
511
+ for (const rule of policy.rules) {
512
+ if (rule.category !== "content" && rule.category !== "import" && rule.category !== "test-integrity")
513
+ continue;
514
+ if (rule.applies_to === "test-files" && !isTestFile(file_path))
515
+ continue;
516
+ if (!matchRule(rule, content))
517
+ continue;
518
+ if (matchRuleUnless(rule, content, context.taskId))
519
+ continue;
520
+ matchedRules.push({
521
+ id: rule.id,
522
+ category: rule.category,
523
+ ...rule.description !== undefined ? { description: rule.description } : {},
524
+ reason: rule.description || `Matched rule ${rule.id}`
525
+ });
526
+ }
527
+ const action = resolveAction(policy.mode, matchedRules);
528
+ return {
529
+ allowed: action !== "block",
530
+ matchedRules,
531
+ action,
532
+ failClosed: false
533
+ };
534
+ }
535
+ function evaluateToolCall(policy, context) {
536
+ const evaluation = context.evaluation;
537
+ if (evaluation.type !== "tool-call") {
538
+ return { allowed: true, matchedRules: [], action: "allow", failClosed: false };
539
+ }
540
+ const { tool_name, tool_input } = evaluation;
541
+ const allMatched = [];
542
+ const filePaths = extractFilePathsFromToolInput(tool_name, tool_input);
543
+ for (const fp of filePaths) {
544
+ const access = isWriteTool(tool_name) ? "write" : "read";
545
+ const scopeResult = evaluateScope(policy, context, fp, access);
546
+ if (scopeResult.matchedRules.length > 0) {
547
+ allMatched.push(...scopeResult.matchedRules);
548
+ }
549
+ }
550
+ const content = extractContentFromToolInput(tool_input);
551
+ if (content) {
552
+ const filePath = filePaths[0] || "";
553
+ const contentContext = {
554
+ ...context,
555
+ evaluation: { type: "content-write", file_path: filePath, content }
556
+ };
557
+ const contentPolicy = loadPolicy(context.projectRoot);
558
+ for (const rule of contentPolicy.rules) {
559
+ if (rule.category !== "content" && rule.category !== "import" && rule.category !== "test-integrity")
560
+ continue;
561
+ if (rule.applies_to === "test-files" && !isTestFile(filePath))
562
+ continue;
563
+ if (!matchRule(rule, content))
564
+ continue;
565
+ if (matchRuleUnless(rule, content, context.taskId))
566
+ continue;
567
+ allMatched.push({
568
+ id: rule.id,
569
+ category: rule.category,
570
+ ...rule.description !== undefined ? { description: rule.description } : {},
571
+ reason: rule.description || `Matched rule ${rule.id}`
572
+ });
573
+ }
574
+ }
575
+ if (tool_name === "Bash") {
576
+ const command = String(tool_input.command || tool_input.cmd || "");
577
+ if (command) {
578
+ const cmdContext = {
579
+ ...context,
580
+ evaluation: { type: "command", command }
581
+ };
582
+ const cmdResult = evaluateCommand(policy, cmdContext);
583
+ if (cmdResult.matchedRules.length > 0) {
584
+ allMatched.push(...cmdResult.matchedRules);
585
+ }
586
+ }
587
+ }
588
+ const seen = new Set;
589
+ const deduplicated = [];
590
+ for (const rule of allMatched) {
591
+ if (!seen.has(rule.id)) {
592
+ seen.add(rule.id);
593
+ deduplicated.push(rule);
594
+ }
595
+ }
596
+ const action = resolveAction(policy.mode, deduplicated);
597
+ return {
598
+ allowed: action !== "block",
599
+ matchedRules: deduplicated,
600
+ action,
601
+ failClosed: false
602
+ };
603
+ }
604
+ function isWriteTool(toolName) {
605
+ return toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit";
606
+ }
607
+ function extractFilePathsFromToolInput(toolName, input) {
608
+ const paths = [];
609
+ const add = (value) => {
610
+ if (typeof value === "string" && value.trim()) {
611
+ paths.push(value.trim());
612
+ }
613
+ };
614
+ if (toolName === "Read" || toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit") {
615
+ add(input.file_path);
616
+ add(input.path);
617
+ } else if (toolName === "Glob") {
618
+ add(input.path);
619
+ } else if (toolName === "Grep") {
620
+ add(input.path);
621
+ } else {
622
+ add(input.file_path);
623
+ add(input.path);
624
+ }
625
+ return paths;
626
+ }
627
+ function extractContentFromToolInput(input) {
628
+ if (typeof input.content === "string")
629
+ return input.content;
630
+ if (typeof input.new_string === "string")
631
+ return input.new_string;
632
+ return "";
633
+ }
634
+ var guardHotPathPrimed = false;
635
+ function primeGuardHotPaths() {
636
+ if (guardHotPathPrimed) {
637
+ return;
638
+ }
639
+ guardHotPathPrimed = true;
640
+ try {
641
+ optimizeNextInvocation(matchRule);
642
+ optimizeNextInvocation(evaluate);
643
+ } catch {}
644
+ }
645
+ primeGuardHotPaths();
646
+
647
+ // packages/cli-surface-plugin/src/control-plane/agent-binary-build.ts
648
+ import { runtimeProvisioningEnv } from "@rig/core/runtime-provisioning-env";
649
+
650
+ // packages/cli-surface-plugin/src/runner.ts
8
651
  class CliError extends RuntimeCliError {
9
- hint;
10
652
  constructor(message, exitCode = 1, options = {}) {
11
- super(message, exitCode);
12
- if (options.hint?.trim()) {
13
- this.hint = options.hint.trim();
653
+ super(message, exitCode, options);
654
+ }
655
+ }
656
+ function formatCommand(parts) {
657
+ return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
658
+ }
659
+ function takeFlag(args, flag) {
660
+ const rest = [];
661
+ let value = false;
662
+ for (const arg of args) {
663
+ if (arg === flag) {
664
+ value = true;
665
+ continue;
666
+ }
667
+ rest.push(arg);
668
+ }
669
+ return { value, rest };
670
+ }
671
+ function takeOption(args, option) {
672
+ const rest = [];
673
+ let value;
674
+ for (let index = 0;index < args.length; index += 1) {
675
+ const current = args[index];
676
+ if (current === option) {
677
+ const next = args[index + 1];
678
+ if (!next || next.startsWith("-")) {
679
+ throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
680
+ }
681
+ value = next;
682
+ index += 1;
683
+ continue;
684
+ }
685
+ if (current !== undefined) {
686
+ rest.push(current);
687
+ }
688
+ }
689
+ return { value, rest };
690
+ }
691
+ function requireNoExtraArgs(args, usage) {
692
+ if (args.length > 0) {
693
+ throw new CliError(`Unexpected arguments: ${args.join(" ")}
694
+ Usage: ${usage}`);
695
+ }
696
+ }
697
+
698
+ // packages/cli-surface-plugin/src/control-plane/native/git-ops.ts
699
+ import { existsSync as existsSync4, lstatSync, mkdirSync, readFileSync as readFileSync3, unlinkSync, writeFileSync } from "fs";
700
+ import { tmpdir } from "os";
701
+ import { dirname, isAbsolute, resolve as resolve3 } from "path";
702
+ import { fileURLToPath } from "url";
703
+ import { loadDotEnvSecrets, resolveRuntimeSecrets } from "@rig/core/baked-secrets";
704
+ import { loadRuntimeContext, loadRuntimeContextFromEnv } from "@rig/core/runtime-context";
705
+
706
+ // packages/cli-surface-plugin/src/control-plane/task-data.ts
707
+ import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
708
+ import { defineCapability } from "@rig/core/capability";
709
+ import { requireInstalledCapability } from "@rig/core/capability-loaders";
710
+ var TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
711
+ function taskData() {
712
+ return requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running CLI git operations.");
713
+ }
714
+
715
+ // packages/cli-surface-plugin/src/control-plane/native/git-ops.ts
716
+ import { nowIso, runCapture as baseRunCapture } from "@rig/core/exec";
717
+ import { resolveCheckoutRoot as resolveMonorepoRoot } from "@rig/core/checkout-root";
718
+ import { getScopeRules as getScopeRules2 } from "@rig/core/scope-rules";
719
+
720
+ // packages/cli-surface-plugin/src/control-plane/native/github-auth-env.ts
721
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
722
+ function cleanToken(value) {
723
+ const trimmed = value?.trim() ?? "";
724
+ return trimmed.length > 0 ? trimmed : null;
725
+ }
726
+ function authStateToken(env = process.env) {
727
+ const file = env.RIG_GITHUB_AUTH_STATE_FILE?.trim();
728
+ if (!file || !existsSync2(file))
729
+ return null;
730
+ try {
731
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
732
+ return cleanToken(typeof parsed.token === "string" ? parsed.token : undefined);
733
+ } catch {
734
+ return null;
735
+ }
736
+ }
737
+
738
+ // packages/cli-surface-plugin/src/control-plane/native/git-ops.ts
739
+ import { safePathSegment } from "@rig/core/safe-identifiers";
740
+
741
+ // packages/cli-surface-plugin/src/control-plane/native/host-git.ts
742
+ import { existsSync as existsSync3 } from "fs";
743
+ import { resolve as resolve2 } from "path";
744
+ function isRuntimeGatewayGitPath(candidate) {
745
+ return /\/\.rig\/bin\/git$/.test(candidate.replace(/\\/g, "/"));
746
+ }
747
+ function isRuntimeGatewayGhPath(candidate) {
748
+ return /\/\.rig\/bin\/gh$/.test(candidate.replace(/\\/g, "/"));
749
+ }
750
+ function resolveHostGitBinary() {
751
+ const candidates = [
752
+ process.env.RIG_GIT_BIN?.trim() || "",
753
+ "/usr/bin/git",
754
+ "/opt/homebrew/bin/git",
755
+ "/usr/local/bin/git"
756
+ ];
757
+ const bunResolved = Bun.which("git");
758
+ if (bunResolved && !isRuntimeGatewayGitPath(bunResolved))
759
+ candidates.push(bunResolved);
760
+ for (const candidate of candidates) {
761
+ if (candidate && !isRuntimeGatewayGitPath(candidate) && existsSync3(candidate))
762
+ return candidate;
763
+ }
764
+ return "git";
765
+ }
766
+ function resolveGithubCliBinary(options = {}) {
767
+ const candidates = new Set;
768
+ const explicit = process.env.RIG_GH_BIN?.trim();
769
+ if (explicit)
770
+ candidates.add(explicit);
771
+ for (const candidate of ["/usr/bin/gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh"])
772
+ candidates.add(candidate);
773
+ if (options.scanPath) {
774
+ for (const entry of (process.env.PATH || "").split(":").map((value) => value.trim()).filter(Boolean)) {
775
+ candidates.add(resolve2(entry, "gh"));
776
+ }
777
+ }
778
+ const bunResolved = Bun.which("gh");
779
+ if (bunResolved)
780
+ candidates.add(bunResolved);
781
+ for (const candidate of candidates) {
782
+ if (candidate && existsSync3(candidate) && !isRuntimeGatewayGhPath(candidate))
783
+ return candidate;
784
+ }
785
+ return "";
786
+ }
787
+
788
+ // packages/cli-surface-plugin/src/control-plane/native/git-ops.ts
789
+ var TASK_RUNTIME_STAGE_EXCLUDES = [
790
+ ".rig/bin/**",
791
+ ".rig/cache/**",
792
+ ".rig/home/**",
793
+ ".rig/logs/**",
794
+ ".rig/runtime/**",
795
+ ".rig/session/**",
796
+ ".rig/state/**",
797
+ ".rig/runtime-context.json"
798
+ ];
799
+ var GENERATED_STAGE_EXCLUDES = ["artifacts/*/runtime-snapshots/**"];
800
+ var TASK_ARTIFACT_STAGE_FALLBACK = new Set([
801
+ "changed-files.txt",
802
+ "contract-changes.md",
803
+ "decision-log.md",
804
+ "git-state.txt",
805
+ "next-actions.md",
806
+ "pr-state.json",
807
+ "task-result.json",
808
+ "validation-summary.json"
809
+ ]);
810
+ function resolveOptionalMonorepoRoot(projectRoot) {
811
+ const runtimeWorkspace = process.env.RIG_TASK_WORKSPACE?.trim();
812
+ if (runtimeWorkspace && existsSync4(resolve3(runtimeWorkspace, ".git"))) {
813
+ return resolve3(runtimeWorkspace);
814
+ }
815
+ try {
816
+ return resolveMonorepoRoot(projectRoot);
817
+ } catch {
818
+ return null;
819
+ }
820
+ }
821
+ function escapeRegExp(value) {
822
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
823
+ }
824
+ function safeCurrentTaskId(projectRoot) {
825
+ try {
826
+ const taskId = taskData().currentTaskId(projectRoot);
827
+ return /^bd-[a-z0-9-]+$/.test(taskId) ? taskId : "";
828
+ } catch {
829
+ return "";
830
+ }
831
+ }
832
+ function gitCmd(projectRoot, repoRoot, ...args) {
833
+ return [resolveHostGitBinary(), "-C", repoRoot, ...args];
834
+ }
835
+ function shouldScopeGitCommit(args, hasTaskContext) {
836
+ if (!hasTaskContext) {
837
+ return false;
838
+ }
839
+ return args.includes("--scoped");
840
+ }
841
+ function gitStatus(projectRoot, taskId) {
842
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
843
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
844
+ const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
845
+ console.log("=== Git Flow Status ===");
846
+ if (resolvedTask) {
847
+ console.log(`Task: ${resolvedTask}`);
848
+ console.log(`Expected monorepo branch: ${expected}`);
849
+ } else {
850
+ console.log("Task: (none active)");
851
+ }
852
+ console.log("");
853
+ printRepoStatus(projectRoot, "project-rig", projectRoot, "");
854
+ const monorepoPath = monorepoRoot || resolveMonorepoRoot(projectRoot);
855
+ if (monorepoPath !== projectRoot) {
856
+ printRepoStatus(projectRoot, "monorepo", monorepoPath, expected);
857
+ }
858
+ }
859
+ function gitChanged(projectRoot, taskId, scoped) {
860
+ if (scoped) {
861
+ const resolvedTask = taskId || taskData().currentTaskId(projectRoot);
862
+ if (!resolvedTask) {
863
+ throw new Error("No task specified and no active task in session. Use --task or omit --scoped.");
864
+ }
865
+ return taskData().changedFilesForTask(projectRoot, resolvedTask, true);
866
+ }
867
+ return taskData().changedFilesForTask(projectRoot, taskId || taskData().currentTaskId(projectRoot) || "", false);
868
+ }
869
+ function gitPreflight(projectRoot, taskId, strict) {
870
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
871
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
872
+ const expected = resolvedTask ? `rig/${resolveTaskBranchId(projectRoot, resolvedTask)}` : "";
873
+ console.log("=== Git Flow Preflight ===");
874
+ let issues = 0;
875
+ if (!existsSync4(resolve3(projectRoot, ".git"))) {
876
+ console.log(`ERROR: project root is not a git repo (${projectRoot})`);
877
+ issues += 1;
878
+ }
879
+ if (monorepoRoot && existsSync4(resolve3(monorepoRoot, ".git"))) {
880
+ const monoBranch = branchName(projectRoot, monorepoRoot);
881
+ if (expected && monoBranch !== expected) {
882
+ console.log(`WARN: monorepo branch is ${monoBranch}, expected ${expected} for task ${resolvedTask}`);
883
+ if (strict) {
884
+ issues += 1;
885
+ }
886
+ }
887
+ const monoChanges = changeCount(projectRoot, monorepoRoot);
888
+ if (monoChanges > 0 && !monoBranch.startsWith("rig/")) {
889
+ console.log(`WARN: monorepo has uncommitted changes on non-rig branch (${monoBranch})`);
890
+ issues += 1;
891
+ }
892
+ } else {
893
+ console.log(`WARN: monorepo repo unavailable.`);
894
+ }
895
+ const projectChanges = changeCount(projectRoot, projectRoot);
896
+ if (projectChanges > 0) {
897
+ console.log(`INFO: project-rig has ${projectChanges} changed file(s).`);
898
+ }
899
+ if (issues > 0) {
900
+ console.log(`Preflight: ${issues} issue(s) detected.`);
901
+ return false;
902
+ }
903
+ console.log("Preflight: OK");
904
+ return true;
905
+ }
906
+ function gitSyncBranch(projectRoot, taskId, targetRepo = "monorepo") {
907
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
908
+ if (!resolvedTask) {
909
+ throw new Error("No task specified and no active task in session.");
910
+ }
911
+ const repoRoot = targetRepo === "monorepo" ? resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot) : projectRoot;
912
+ const repoLabel = targetRepo === "monorepo" ? "Monorepo" : "Project";
913
+ if (!existsSync4(resolve3(repoRoot, ".git"))) {
914
+ throw new Error(`${repoLabel} repo not found at ${repoRoot}`);
915
+ }
916
+ const branchId = resolveTaskBranchId(projectRoot, resolvedTask);
917
+ const branchTarget = `rig/${branchId}`;
918
+ const current = branchName(projectRoot, repoRoot);
919
+ if (current === branchTarget) {
920
+ console.log(`${repoLabel} branch: already on ${branchTarget}`);
921
+ return;
922
+ }
923
+ const hasBranch = runCapture(gitCmd(projectRoot, repoRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branchTarget}`), projectRoot).exitCode === 0;
924
+ const cmd = hasBranch && current === "HEAD" ? gitCmd(projectRoot, repoRoot, "checkout", "-B", branchTarget) : hasBranch ? gitCmd(projectRoot, repoRoot, "checkout", branchTarget) : gitCmd(projectRoot, repoRoot, "checkout", "-b", branchTarget);
925
+ const checkout = runCapture(cmd, projectRoot);
926
+ if (checkout.exitCode !== 0) {
927
+ throw new Error(`Failed to sync ${repoLabel.toLowerCase()} branch: ${checkout.stderr || checkout.stdout}`);
928
+ }
929
+ const action = hasBranch && current === "HEAD" ? "reset" : hasBranch ? "checked out" : "created";
930
+ console.log(`${repoLabel} branch: ${action} ${branchTarget}`);
931
+ }
932
+ function gitCommit(options) {
933
+ const { projectRoot } = options;
934
+ const resolvedTask = options.taskId || safeCurrentTaskId(projectRoot);
935
+ const baseMessage = options.message || (resolvedTask ? `rig: ${resolvedTask}` : "rig: harness update");
936
+ const changedFilesManifest = resolvedTask && options.scoped === true ? refreshChangedFilesManifest(projectRoot, resolvedTask) : "";
937
+ const changedFiles = resolvedTask && options.scoped === true ? readChangedFilesManifest(projectRoot, resolvedTask) : resolvedTask ? taskData().changedFilesForTask(projectRoot, resolvedTask, false) : [];
938
+ if (options.target === "project" || options.target === "both") {
939
+ if (resolvedTask) {
940
+ gitSyncBranch(projectRoot, resolvedTask, "project");
941
+ }
942
+ const projectFiles = resolveScopedStageFilesForRepo(projectRoot, projectRoot, resolvedTask, changedFiles);
943
+ commitRepo(projectRoot, projectRoot, "project-rig", options.target === "both" ? `${baseMessage} [harness]` : baseMessage, options.allowEmpty, options.scoped === true, projectFiles, changedFilesManifest);
944
+ }
945
+ if (options.target === "monorepo" || options.target === "both") {
946
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot) || resolveMonorepoRoot(projectRoot);
947
+ if (resolvedTask) {
948
+ gitSyncBranch(projectRoot, resolvedTask, "monorepo");
949
+ }
950
+ const monorepoFiles = resolveScopedStageFilesForRepo(projectRoot, monorepoRoot, resolvedTask, changedFiles);
951
+ commitRepo(projectRoot, monorepoRoot, "monorepo", options.target === "both" ? `${baseMessage} [monorepo]` : baseMessage, options.allowEmpty, options.scoped === true, monorepoFiles, changedFilesManifest);
952
+ }
953
+ }
954
+ function gitSnapshot(projectRoot, taskId, outputPath) {
955
+ const monorepoRoot = resolveOptionalMonorepoRoot(projectRoot);
956
+ const resolvedTask = taskId || safeCurrentTaskId(projectRoot);
957
+ const output = outputPath || (resolvedTask ? resolveArtifactSnapshot(projectRoot, resolvedTask) : resolve3(resolve3(projectRoot, ".rig", "state"), "git-state.txt"));
958
+ mkdirSync(dirname(output), { recursive: true });
959
+ const lines = ["# Git Snapshot", `timestamp: ${nowIso()}`];
960
+ if (resolvedTask) {
961
+ lines.push(`task: ${resolvedTask}`);
962
+ }
963
+ lines.push("");
964
+ lines.push(...snapshotRepo(projectRoot, "project-rig", projectRoot));
965
+ if (monorepoRoot && monorepoRoot !== projectRoot) {
966
+ lines.push(...snapshotRepo(projectRoot, "monorepo", monorepoRoot));
967
+ }
968
+ writeFileSync(output, `${lines.join(`
969
+ `)}
970
+ `, "utf-8");
971
+ return output;
972
+ }
973
+ function gitOpenPr(options) {
974
+ const gh = resolveGithubCliBinary({ scanPath: true });
975
+ if (!gh) {
976
+ throw new Error("gh CLI is required for open-pr. Install and authenticate with: gh auth login");
977
+ }
978
+ const taskId = options.taskId || safeCurrentTaskId(options.projectRoot);
979
+ const target = options.target || (taskId ? "monorepo" : "project");
980
+ let repoRoot = options.projectRoot;
981
+ let repoLabel = "project-rig";
982
+ const envBase = target === "monorepo" ? process.env.RIG_PR_BASE_MONOREPO?.trim() || "" : process.env.RIG_PR_BASE_PROJECT?.trim() || "";
983
+ if (target === "monorepo") {
984
+ repoRoot = resolveOptionalMonorepoRoot(options.projectRoot) || resolveMonorepoRoot(options.projectRoot);
985
+ repoLabel = "monorepo";
986
+ if (taskId) {
987
+ gitSyncBranch(options.projectRoot, taskId, "monorepo");
988
+ }
989
+ } else if (taskId) {
990
+ gitSyncBranch(options.projectRoot, taskId, "project");
991
+ }
992
+ if (!existsSync4(resolve3(repoRoot, ".git"))) {
993
+ throw new Error(`Repository not available for open-pr target ${target}: ${repoRoot}`);
994
+ }
995
+ const branch = branchName(options.projectRoot, repoRoot);
996
+ if (!branch || branch === "HEAD") {
997
+ throw new Error(`Cannot open PR from detached HEAD in ${repoLabel}. Checkout a branch first.`);
998
+ }
999
+ const repoNameWithOwner = resolveRepoNameWithOwner(options.projectRoot, repoRoot);
1000
+ const networkRemote = resolveNetworkRemoteName(options.projectRoot, repoRoot, repoNameWithOwner);
1001
+ const base = options.base || envBase || inferRepositoryDefaultBase(options.projectRoot, repoRoot, repoNameWithOwner, networkRemote, target === "project" ? inferProjectBase(options.projectRoot, "main") : "main");
1002
+ refreshRemoteBaseRef(options.projectRoot, repoRoot, base);
1003
+ let reviewer = (options.reviewer || "").trim();
1004
+ let reviewerSource = reviewer ? "flag" : undefined;
1005
+ if (!reviewer && taskId) {
1006
+ reviewer = defaultReviewerForTask(options.projectRoot, taskId);
1007
+ if (reviewer) {
1008
+ reviewerSource = "task-config";
1009
+ }
1010
+ }
1011
+ if (!reviewer) {
1012
+ reviewer = inferReviewerFromChangedFiles(options.projectRoot, repoRoot, base, branch);
1013
+ if (reviewer) {
1014
+ reviewerSource = "changed-files";
1015
+ }
1016
+ }
1017
+ if (!reviewer) {
1018
+ reviewer = (process.env.RIG_PR_REVIEWER || "").trim();
1019
+ if (reviewer) {
1020
+ reviewerSource = "env";
1021
+ }
1022
+ }
1023
+ let title = options.title || "";
1024
+ if (!title) {
1025
+ if (taskId) {
1026
+ title = `rig: ${taskId}`;
1027
+ } else {
1028
+ title = `rig: update ${branch}`;
1029
+ }
1030
+ }
1031
+ const body = options.body || [
1032
+ "## Summary",
1033
+ "- Automated task output prepared in isolated runtime.",
1034
+ "",
1035
+ "## Task",
1036
+ `- beads: ${taskId || "n/a"}`,
1037
+ ...defaultPrRunLines(taskId, repoNameWithOwner),
1038
+ "",
1039
+ "## Review",
1040
+ "- Completion verification will run validation, verifier review, and PR policy checks.",
1041
+ "- When repository policy allows it, Rig attempts an immediate strict-gated, head-locked merge after approval."
1042
+ ].join(`
1043
+ `);
1044
+ const preCheck = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
1045
+ const preCheckEntry = preCheck.exitCode === 0 ? preCheck.stdout.trim() : "";
1046
+ if (preCheckEntry && preCheckEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
1047
+ const mergedPr = JSON.parse(preCheckEntry);
1048
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
1049
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
1050
+ if (taskId)
1051
+ writePrMetadata(options.projectRoot, taskId, result2);
1052
+ return result2;
1053
+ }
1054
+ const pushArgs = gitCmd(options.projectRoot, repoRoot, "push", "-u", networkRemote, branch);
1055
+ const fetchResult = runCapture(gitCmd(options.projectRoot, repoRoot, "fetch", networkRemote, branch), repoRoot);
1056
+ if (fetchResult.exitCode === 0) {
1057
+ const remoteAhead = runCapture(gitCmd(options.projectRoot, repoRoot, "log", "--oneline", `HEAD..${networkRemote}/${branch}`), repoRoot);
1058
+ if (remoteAhead.exitCode === 0 && remoteAhead.stdout.trim()) {
1059
+ console.log(`Remote branch has diverged \u2014 force pushing task-owned branch ${branch} with --force-with-lease...`);
1060
+ pushArgs.splice(4, 0, "--force-with-lease");
1061
+ }
1062
+ }
1063
+ runOrThrow(options.projectRoot, pushArgs, `Failed to push branch ${branch} in ${repoLabel}`);
1064
+ const existing = runCapture(withGhRepo([gh, "pr", "list", "--state", "open", "--head", branch, "--json", "url", "--jq", ".[0].url"], repoNameWithOwner), repoRoot);
1065
+ const existingUrl = existing.exitCode === 0 ? existing.stdout.trim() : "";
1066
+ if (!existingUrl || existingUrl === "null") {
1067
+ const merged = runCapture(withGhRepo([gh, "pr", "list", "--state", "merged", "--head", branch, "--json", "url,mergedAt", "--jq", ".[0]"], repoNameWithOwner), repoRoot);
1068
+ const mergedEntry = merged.exitCode === 0 ? merged.stdout.trim() : "";
1069
+ if (mergedEntry && mergedEntry !== "null" && currentHeadMatchesMergedBase(options.projectRoot, repoRoot, base, networkRemote)) {
1070
+ const mergedPr = JSON.parse(mergedEntry);
1071
+ console.log(`Branch ${branch} was already merged: ${mergedPr.url}`);
1072
+ const result2 = { url: mergedPr.url, target, repoLabel, branch, base };
1073
+ if (taskId)
1074
+ writePrMetadata(options.projectRoot, taskId, result2);
1075
+ return result2;
1076
+ }
1077
+ }
1078
+ let prUrl = "";
1079
+ if (existingUrl && existingUrl !== "null") {
1080
+ prUrl = existingUrl;
1081
+ } else {
1082
+ const createArgs = [
1083
+ gh,
1084
+ "pr",
1085
+ "create",
1086
+ ...ghRepoArgs(repoNameWithOwner),
1087
+ "--base",
1088
+ base,
1089
+ "--head",
1090
+ branch,
1091
+ "--title",
1092
+ title,
1093
+ "--body",
1094
+ body
1095
+ ];
1096
+ if (options.draft) {
1097
+ createArgs.push("--draft");
1098
+ }
1099
+ const created = runCapture(createArgs, repoRoot);
1100
+ if (created.exitCode !== 0) {
1101
+ throw new Error(`Failed to create PR in ${repoLabel}: ${created.stderr || created.stdout}`);
1102
+ }
1103
+ prUrl = created.stdout.trim();
1104
+ }
1105
+ if (!prUrl) {
1106
+ throw new Error(`Failed to resolve PR URL for branch ${branch}.`);
1107
+ }
1108
+ assertPrHasNoGitConflicts(readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl), repoLabel, base);
1109
+ if (reviewer) {
1110
+ const edit = runCapture(withGhRepo([gh, "pr", "edit", prUrl, "--add-reviewer", reviewer], repoNameWithOwner), repoRoot);
1111
+ if (edit.exitCode !== 0) {
1112
+ throw new Error(`Failed to assign reviewer '${reviewer}': ${edit.stderr || edit.stdout}`);
1113
+ }
1114
+ }
1115
+ const result = {
1116
+ url: prUrl,
1117
+ ...reviewer ? { reviewer } : {},
1118
+ ...reviewerSource ? { reviewerSource } : {},
1119
+ target,
1120
+ repoLabel,
1121
+ branch,
1122
+ base
1123
+ };
1124
+ if (taskId) {
1125
+ writePrMetadata(options.projectRoot, taskId, result);
1126
+ }
1127
+ return result;
1128
+ }
1129
+ function defaultPrRunLines(taskId, repoNameWithOwner) {
1130
+ const lines = [];
1131
+ const runId = process.env.RIG_SERVER_RUN_ID?.trim();
1132
+ if (runId) {
1133
+ lines.push(`- Run: ${runId}`);
1134
+ }
1135
+ const closeout = defaultPrCloseoutLine(taskId, repoNameWithOwner);
1136
+ if (closeout) {
1137
+ lines.push(`- ${closeout}`);
1138
+ }
1139
+ return lines;
1140
+ }
1141
+ function defaultPrCloseoutLine(taskId, repoNameWithOwner) {
1142
+ const sourceIssueId = loadRuntimeContextFromEnv()?.sourceTask?.sourceIssueId;
1143
+ if (sourceIssueId) {
1144
+ const match = sourceIssueId.match(/^([^#]+)#(\d+)$/);
1145
+ if (match?.[1] && match[2]) {
1146
+ const sourceRepo = match[1];
1147
+ const issueNumber = match[2];
1148
+ return sourceRepo.toLowerCase() === repoNameWithOwner.toLowerCase() ? `Closes #${issueNumber}` : `Closes ${sourceRepo}#${issueNumber}`;
1149
+ }
1150
+ }
1151
+ return /^\d+$/.test(taskId) ? `Closes #${taskId}` : "";
1152
+ }
1153
+ function readPrViewState(gh, repoRoot, repoNameWithOwner, prUrl) {
1154
+ const view = runCapture(withGhRepo([
1155
+ gh,
1156
+ "pr",
1157
+ "view",
1158
+ prUrl,
1159
+ "--json",
1160
+ "state,isDraft,url,mergedAt,autoMergeRequest,mergeable,mergeStateStatus,reviewDecision,headRefOid,statusCheckRollup",
1161
+ "--jq",
1162
+ "."
1163
+ ], repoNameWithOwner), repoRoot);
1164
+ if (view.exitCode !== 0) {
1165
+ throw new Error(`Failed to inspect PR ${prUrl}: ${view.stderr || view.stdout}`);
1166
+ }
1167
+ try {
1168
+ const parsed = JSON.parse(view.stdout);
1169
+ return {
1170
+ state: parsed.state || "OPEN",
1171
+ isDraft: parsed.isDraft === true,
1172
+ url: typeof parsed.url === "string" ? parsed.url : prUrl,
1173
+ mergedAt: typeof parsed.mergedAt === "string" ? parsed.mergedAt : null,
1174
+ autoMergeRequest: parsed.autoMergeRequest ?? null,
1175
+ mergeable: typeof parsed.mergeable === "string" ? parsed.mergeable : "",
1176
+ mergeStateStatus: typeof parsed.mergeStateStatus === "string" ? parsed.mergeStateStatus : "",
1177
+ reviewDecision: typeof parsed.reviewDecision === "string" ? parsed.reviewDecision : "",
1178
+ headRefOid: typeof parsed.headRefOid === "string" ? parsed.headRefOid : null,
1179
+ statusCheckRollup: Array.isArray(parsed.statusCheckRollup) ? parsed.statusCheckRollup : []
1180
+ };
1181
+ } catch {
1182
+ return {
1183
+ state: "OPEN",
1184
+ isDraft: false,
1185
+ url: prUrl,
1186
+ mergedAt: null,
1187
+ autoMergeRequest: null,
1188
+ mergeable: "",
1189
+ mergeStateStatus: "",
1190
+ reviewDecision: "",
1191
+ headRefOid: null,
1192
+ statusCheckRollup: []
1193
+ };
1194
+ }
1195
+ }
1196
+ function assertPrHasNoGitConflicts(prState, repoLabel, baseRef) {
1197
+ const mergeable = prState.mergeable.toUpperCase();
1198
+ const mergeStateStatus = prState.mergeStateStatus.toUpperCase();
1199
+ if (mergeable === "CONFLICTING" || mergeStateStatus === "DIRTY") {
1200
+ 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.`);
1201
+ }
1202
+ }
1203
+ function writePrMetadata(projectRoot, taskId, result) {
1204
+ const dir = taskData().artifactDirForId(projectRoot, taskId);
1205
+ mkdirSync(dir, { recursive: true });
1206
+ const path = resolve3(dir, "pr-state.json");
1207
+ let prs = {};
1208
+ if (existsSync4(path)) {
1209
+ try {
1210
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
1211
+ if (parsed && typeof parsed === "object" && parsed.prs && typeof parsed.prs === "object") {
1212
+ prs = parsed.prs;
1213
+ }
1214
+ } catch {
1215
+ prs = {};
1216
+ }
1217
+ }
1218
+ prs[result.target] = result;
1219
+ const primary = prs.monorepo || prs.project;
1220
+ const artifact = {
1221
+ task_id: taskId,
1222
+ prs,
1223
+ ...primary || {},
1224
+ updated_at: nowIso()
1225
+ };
1226
+ writeFileSync(path, `${JSON.stringify(artifact, null, 2)}
1227
+ `, "utf-8");
1228
+ }
1229
+ function resolveArtifactSnapshot(projectRoot, taskId) {
1230
+ return resolve3(taskData().artifactDirForId(projectRoot, taskId), "git-state.txt");
1231
+ }
1232
+ function ensureFullGitHistory(projectRoot, repoRoot, remoteName = "origin") {
1233
+ const shallow = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--is-shallow-repository"), projectRoot);
1234
+ if (shallow.exitCode !== 0 || shallow.stdout.trim() !== "true") {
1235
+ return;
1236
+ }
1237
+ const unshallow = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--unshallow", "--tags", remoteName), projectRoot);
1238
+ if (unshallow.exitCode === 0) {
1239
+ return;
1240
+ }
1241
+ const output = `${unshallow.stderr}
1242
+ ${unshallow.stdout}`.trim();
1243
+ if (/--unshallow on a complete repository|does not make sense/i.test(output)) {
1244
+ return;
1245
+ }
1246
+ throw new Error(`Failed to expand git history for ${repoRoot}: ${output}`);
1247
+ }
1248
+ function refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner = "") {
1249
+ const remoteName = resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner || resolveRepoNameWithOwner(projectRoot, repoRoot));
1250
+ const remoteUrl = runCapture(gitCmd(projectRoot, repoRoot, "remote", "get-url", remoteName), projectRoot);
1251
+ if (remoteUrl.exitCode !== 0) {
1252
+ return "";
1253
+ }
1254
+ ensureFullGitHistory(projectRoot, repoRoot, remoteName);
1255
+ const fetch = runCapture(gitCmd(projectRoot, repoRoot, "fetch", "--prune", "--tags", remoteName, `+refs/heads/${baseRef}:refs/remotes/${remoteName}/${baseRef}`), projectRoot);
1256
+ if (fetch.exitCode !== 0) {
1257
+ throw new Error(`Failed to refresh ${remoteName}/${baseRef} at ${repoRoot}: ${fetch.stderr || fetch.stdout}`);
1258
+ }
1259
+ return remoteName;
1260
+ }
1261
+ function currentHeadMatchesMergedBase(projectRoot, repoRoot, baseRef, remoteName = "origin") {
1262
+ const activeRemote = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef) || remoteName;
1263
+ const remoteBase = `${activeRemote}/${baseRef}`;
1264
+ const hasRemoteBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", remoteBase), repoRoot).exitCode === 0;
1265
+ const targetRef = hasRemoteBase ? remoteBase : baseRef;
1266
+ if (runCapture(gitCmd(projectRoot, repoRoot, "merge-base", "--is-ancestor", "HEAD", targetRef), repoRoot).exitCode === 0) {
1267
+ return true;
1268
+ }
1269
+ return runCapture(gitCmd(projectRoot, repoRoot, "diff", "--quiet", "HEAD", targetRef, "--"), repoRoot).exitCode === 0;
1270
+ }
1271
+ function defaultReviewerForTask(projectRoot, taskId) {
1272
+ if (!taskId) {
1273
+ return "";
1274
+ }
1275
+ const entry = taskData().readTaskConfig(projectRoot)[taskId];
1276
+ const reviewer = entry?.reviewer;
1277
+ if (typeof reviewer === "string" && reviewer.trim()) {
1278
+ return reviewer.trim();
1279
+ }
1280
+ const responsibleReviewer = entry?.responsible_reviewer;
1281
+ if (typeof responsibleReviewer === "string" && responsibleReviewer.trim()) {
1282
+ return responsibleReviewer.trim();
1283
+ }
1284
+ return "";
1285
+ }
1286
+ function resolveRepoNameWithOwner(projectRoot, repoRoot) {
1287
+ const explicit = normalizeGithubRepoNameWithOwner(process.env.GH_REPO || "");
1288
+ if (explicit) {
1289
+ return explicit;
1290
+ }
1291
+ const visited = new Set;
1292
+ return resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, repoRoot, repoRoot, visited);
1293
+ }
1294
+ function resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, gitRoot, cwd, visited) {
1295
+ const normalizedGitRoot = resolve3(gitRoot);
1296
+ if (visited.has(normalizedGitRoot)) {
1297
+ return "";
1298
+ }
1299
+ visited.add(normalizedGitRoot);
1300
+ const remotes = listGitRemotes(projectRoot, gitRoot, cwd);
1301
+ for (const remote of remotes) {
1302
+ const urls = listGitRemoteUrls(projectRoot, gitRoot, cwd, remote);
1303
+ for (const url of urls) {
1304
+ const direct = normalizeGithubRepoNameWithOwner(url);
1305
+ if (direct) {
1306
+ return direct;
1307
+ }
1308
+ const localGitRoot = resolveLocalGitRemoteRoot(url, gitRoot);
1309
+ if (!localGitRoot) {
1310
+ continue;
1311
+ }
1312
+ const viaMirror = resolveGithubRepoNameWithOwnerFromGitRoot(projectRoot, localGitRoot, cwd, visited);
1313
+ if (viaMirror) {
1314
+ return viaMirror;
1315
+ }
1316
+ }
1317
+ }
1318
+ return "";
1319
+ }
1320
+ function listGitRemotes(projectRoot, gitRoot, cwd) {
1321
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote");
1322
+ if (result.exitCode !== 0) {
1323
+ return [];
1324
+ }
1325
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1326
+ }
1327
+ function listGitRemoteUrls(projectRoot, gitRoot, cwd, remote) {
1328
+ const result = gitQuery(projectRoot, gitRoot, cwd, "remote", "get-url", "--all", remote);
1329
+ if (result.exitCode !== 0) {
1330
+ return [];
1331
+ }
1332
+ return result.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1333
+ }
1334
+ function resolveNetworkRemoteName(projectRoot, repoRoot, repoNameWithOwner) {
1335
+ const remotes = listGitRemotes(projectRoot, repoRoot, repoRoot);
1336
+ if (remotes.length === 0) {
1337
+ return "origin";
1338
+ }
1339
+ if (!repoNameWithOwner) {
1340
+ return remotes.includes("origin") ? "origin" : remotes[0];
1341
+ }
1342
+ const expectedRepo = normalizeGithubRepoNameWithOwner(repoNameWithOwner).toLowerCase();
1343
+ let directMatch = "";
1344
+ for (const remote of remotes) {
1345
+ const urls = listGitRemoteUrls(projectRoot, repoRoot, repoRoot, remote);
1346
+ for (const url of urls) {
1347
+ const direct = normalizeGithubRepoNameWithOwner(url);
1348
+ if (direct && direct.toLowerCase() === expectedRepo) {
1349
+ if (remote === "github") {
1350
+ return remote;
1351
+ }
1352
+ if (!directMatch) {
1353
+ directMatch = remote;
1354
+ }
1355
+ }
14
1356
  }
15
1357
  }
1358
+ if (remotes.includes("github")) {
1359
+ return "github";
1360
+ }
1361
+ if (directMatch) {
1362
+ return directMatch;
1363
+ }
1364
+ return remotes.includes("origin") ? "origin" : remotes[0];
16
1365
  }
17
- function formatCommand(parts) {
18
- return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
1366
+ function gitQuery(projectRoot, gitRoot, cwd, ...args) {
1367
+ const gitArgs = existsSync4(resolve3(gitRoot, ".git")) ? gitCmd(projectRoot, gitRoot, ...args) : [resolveHostGitBinary(), "--git-dir", gitRoot, ...args];
1368
+ return runCapture(gitArgs, cwd, projectRoot);
19
1369
  }
20
- function takeFlag(args, flag) {
21
- const rest = [];
22
- let value = false;
23
- for (const arg of args) {
24
- if (arg === flag) {
25
- value = true;
1370
+ function resolveLocalGitRemoteRoot(remoteUrl, gitRoot) {
1371
+ const normalized = remoteUrl.trim();
1372
+ if (!normalized) {
1373
+ return "";
1374
+ }
1375
+ let candidate = normalized;
1376
+ if (normalized.startsWith("file://")) {
1377
+ try {
1378
+ candidate = fileURLToPath(normalized);
1379
+ } catch {
1380
+ return "";
1381
+ }
1382
+ } else if (/^[a-z][a-z0-9+.-]*:\/\//i.test(normalized) || /^[^@]+@[^:]+:.+$/.test(normalized)) {
1383
+ return "";
1384
+ } else if (!isAbsolute(normalized)) {
1385
+ candidate = resolve3(gitRoot, normalized);
1386
+ }
1387
+ return existsSync4(candidate) ? candidate : "";
1388
+ }
1389
+ function normalizeGithubRepoNameWithOwner(value) {
1390
+ const normalized = value.trim();
1391
+ if (!normalized) {
1392
+ return "";
1393
+ }
1394
+ const scpMatch = normalized.match(/^(?:ssh:\/\/)?git@github\.com[:/](.+?)(?:\.git)?$/i);
1395
+ if (scpMatch?.[1]) {
1396
+ return scpMatch[1].replace(/^\/+|\/+$/g, "");
1397
+ }
1398
+ const httpMatch = normalized.match(/^https?:\/\/github\.com\/(.+?)(?:\.git)?(?:\/)?$/i);
1399
+ if (httpMatch?.[1]) {
1400
+ return httpMatch[1].replace(/^\/+|\/+$/g, "");
1401
+ }
1402
+ const bareMatch = normalized.match(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/);
1403
+ return bareMatch ? bareMatch[0] : "";
1404
+ }
1405
+ function ghRepoArgs(repoNameWithOwner) {
1406
+ return repoNameWithOwner ? ["-R", repoNameWithOwner] : [];
1407
+ }
1408
+ function withGhRepo(command, repoNameWithOwner) {
1409
+ if (!repoNameWithOwner || command.length < 3) {
1410
+ return command;
1411
+ }
1412
+ return [command[0], command[1], command[2], ...ghRepoArgs(repoNameWithOwner), ...command.slice(3)];
1413
+ }
1414
+ function inferRepositoryDefaultBase(projectRoot, repoRoot, repoNameWithOwner, remoteName, fallback) {
1415
+ const remote = remoteName || "origin";
1416
+ const symbolic = runCapture(gitCmd(projectRoot, repoRoot, "symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`), projectRoot);
1417
+ if (symbolic.exitCode === 0) {
1418
+ const ref = symbolic.stdout.trim().replace(new RegExp(`^${escapeRegExp(remote)}/`), "");
1419
+ if (ref && ref !== "HEAD") {
1420
+ return ref;
1421
+ }
1422
+ }
1423
+ const lsRemote = runCapture(gitCmd(projectRoot, repoRoot, "ls-remote", "--symref", remote, "HEAD"), projectRoot);
1424
+ if (lsRemote.exitCode === 0) {
1425
+ const match = lsRemote.stdout.match(/^ref:\s+refs\/heads\/([^\t\r\n]+)\s+HEAD/m);
1426
+ if (match?.[1]) {
1427
+ return match[1];
1428
+ }
1429
+ }
1430
+ const gh = resolveGithubCliBinary({ scanPath: true });
1431
+ if (gh && repoNameWithOwner) {
1432
+ const api = runCapture(withGhRepo([gh, "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"], repoNameWithOwner), repoRoot);
1433
+ const branch = api.exitCode === 0 ? api.stdout.trim() : "";
1434
+ if (branch) {
1435
+ return branch;
1436
+ }
1437
+ }
1438
+ return fallback;
1439
+ }
1440
+ function inferProjectBase(projectRoot, fallback) {
1441
+ const containing = runCapture(gitCmd(projectRoot, projectRoot, "branch", "-r", "--contains", "HEAD"), projectRoot);
1442
+ if (containing.exitCode !== 0) {
1443
+ return fallback;
1444
+ }
1445
+ 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/"));
1446
+ return candidates[0] || fallback;
1447
+ }
1448
+ function currentGithubLogin(repoRoot) {
1449
+ const gh = resolveGithubCliBinary({ scanPath: true });
1450
+ if (!gh) {
1451
+ return "";
1452
+ }
1453
+ const result = runCapture([gh, "api", "user", "--jq", ".login"], repoRoot);
1454
+ return result.exitCode === 0 ? result.stdout.trim() : "";
1455
+ }
1456
+ function collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
1457
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
1458
+ const remoteName = refreshRemoteBaseRef(projectRoot, repoRoot, baseRef, repoNameWithOwner);
1459
+ const hasRemoteBase = remoteName ? runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", `${remoteName}/${baseRef}`), projectRoot).exitCode === 0 : false;
1460
+ const hasLocalBase = runCapture(gitCmd(projectRoot, repoRoot, "rev-parse", "--verify", "--quiet", baseRef), projectRoot).exitCode === 0;
1461
+ let changed = "";
1462
+ if (hasRemoteBase) {
1463
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${remoteName}/${baseRef}...${branchRef}`), projectRoot).stdout;
1464
+ } else if (hasLocalBase) {
1465
+ changed = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `${baseRef}...${branchRef}`), projectRoot).stdout;
1466
+ } else {
1467
+ const fallback = runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only", `HEAD~1..${branchRef}`), projectRoot);
1468
+ changed = fallback.exitCode === 0 ? fallback.stdout : runCapture(gitCmd(projectRoot, repoRoot, "diff", "--name-only"), projectRoot).stdout;
1469
+ }
1470
+ return changed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(0, 60);
1471
+ }
1472
+ function inferReviewerFromChangedFiles(projectRoot, repoRoot, baseRef, branchRef) {
1473
+ const repoNameWithOwner = resolveRepoNameWithOwner(projectRoot, repoRoot);
1474
+ if (!repoNameWithOwner) {
1475
+ return "";
1476
+ }
1477
+ const actorLogin = currentGithubLogin(repoRoot);
1478
+ const changedFiles = collectPrChangedFiles(projectRoot, repoRoot, baseRef, branchRef);
1479
+ if (changedFiles.length === 0) {
1480
+ return "";
1481
+ }
1482
+ const counts = new Map;
1483
+ for (const path of changedFiles) {
1484
+ const result = runCapture([
1485
+ resolveGithubCliBinary({ scanPath: true }) || "gh",
1486
+ "api",
1487
+ `repos/${repoNameWithOwner}/commits`,
1488
+ "-f",
1489
+ `path=${path}`,
1490
+ "-f",
1491
+ `sha=${baseRef}`,
1492
+ "-f",
1493
+ "per_page=1",
1494
+ "--jq",
1495
+ ".[0].author.login // empty"
1496
+ ], repoRoot);
1497
+ const author = result.exitCode === 0 ? result.stdout.trim() : "";
1498
+ if (!author || author === actorLogin) {
26
1499
  continue;
27
1500
  }
28
- rest.push(arg);
1501
+ counts.set(author, (counts.get(author) || 0) + 1);
29
1502
  }
30
- return { value, rest };
1503
+ let best = "";
1504
+ let max = -1;
1505
+ for (const [author, count] of counts.entries()) {
1506
+ if (count > max || count === max && author < best) {
1507
+ best = author;
1508
+ max = count;
1509
+ }
1510
+ }
1511
+ return best;
31
1512
  }
32
- function takeOption(args, option) {
1513
+ function snapshotRepo(projectRoot, label, repo) {
1514
+ if (!existsSync4(resolve3(repo, ".git"))) {
1515
+ return [`## ${label}`, `repo: ${repo}`, "status: unavailable", ""];
1516
+ }
1517
+ const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
1518
+ const branch = branchName(projectRoot, repo);
1519
+ const head = runCapture(gitCmd(projectRoot, repo, "rev-parse", "HEAD"), projectRoot).stdout.trim();
1520
+ return [
1521
+ `## ${label}`,
1522
+ `repo: ${repo}`,
1523
+ `branch: ${branch}`,
1524
+ `head: ${head}`,
1525
+ "status:",
1526
+ status || "(clean)",
1527
+ ""
1528
+ ];
1529
+ }
1530
+ function commitRepo(projectRoot, repo, label, message, allowEmpty, scoped, files, changedFilesManifest) {
1531
+ if (!existsSync4(resolve3(repo, ".git"))) {
1532
+ console.log(`Skipping ${label}: repo not available (${repo})`);
1533
+ return;
1534
+ }
1535
+ const scopedFiles = (files || []).filter(Boolean).filter((file) => !pathResolvesBeyondSymlink(repo, file));
1536
+ const repoChanges = changeCount(projectRoot, repo);
1537
+ if (scopedFiles.length === 0 && repoChanges === 0 && !allowEmpty) {
1538
+ console.log(`Skipping ${label}: no changes to commit.`);
1539
+ return;
1540
+ }
1541
+ if (scopedFiles.length > 0) {
1542
+ const indexFiles = new Set(runCapture(gitCmd(projectRoot, repo, "ls-files"), projectRoot).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean));
1543
+ const stageable = scopedFiles.filter((file) => indexFiles.has(file) || existsSync4(resolve3(repo, file)));
1544
+ if (stageable.length === 0) {
1545
+ console.log(`Skipping ${label}: collected change list matched no stageable paths in ${repo}.`);
1546
+ return;
1547
+ }
1548
+ const pathspecFile = resolve3(tmpdir(), `rig-stage-${process.pid}-${Date.now()}.txt`);
1549
+ writeFileSync(pathspecFile, `${stageable.join(`
1550
+ `)}
1551
+ `, "utf-8");
1552
+ try {
1553
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "add", `--pathspec-from-file=${pathspecFile}`), `Failed to stage changes for ${label}`);
1554
+ } finally {
1555
+ try {
1556
+ unlinkSync(pathspecFile);
1557
+ } catch {}
1558
+ }
1559
+ } else {
1560
+ const addArgs = buildStageAddArgs(repo, scopedFiles, scoped);
1561
+ if (addArgs) {
1562
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, ...addArgs), `Failed to stage changes for ${label}`);
1563
+ }
1564
+ }
1565
+ const stagedChanges = stagedChangeCount(projectRoot, repo);
1566
+ if (stagedChanges === 0) {
1567
+ if (allowEmpty) {
1568
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", "--allow-empty", "-m", message), `Failed to commit ${label}`);
1569
+ console.log(`Committed ${label}: ${message}`);
1570
+ return;
1571
+ }
1572
+ if (scoped && repoChanges > 0) {
1573
+ const manifestHint = changedFilesManifest ? ` Refresh ${changedFilesManifest} and retry, or use --all/--unscoped intentionally.` : "";
1574
+ throw new Error(`Scoped commit for ${label} resolved no stageable files.${manifestHint}`);
1575
+ }
1576
+ console.log(`Skipping ${label}: no stageable changes to commit.`);
1577
+ return;
1578
+ }
1579
+ runOrThrow(projectRoot, gitCmd(projectRoot, repo, "commit", ...allowEmpty ? ["--allow-empty"] : [], "-m", message), `Failed to commit ${label}`);
1580
+ console.log(`Committed ${label}: ${message}`);
1581
+ }
1582
+ function readChangedFilesManifest(projectRoot, taskId) {
1583
+ const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1584
+ if (!existsSync4(manifestPath)) {
1585
+ return [];
1586
+ }
1587
+ const files = readFileSync3(manifestPath, "utf-8").split(/\r?\n/).map((line) => normalizeChangedFilePath(line)).filter(Boolean);
1588
+ return [...new Set(files)];
1589
+ }
1590
+ function refreshChangedFilesManifest(projectRoot, taskId) {
1591
+ const manifestPath = resolve3(taskData().artifactDirForId(projectRoot, taskId), "changed-files.txt");
1592
+ mkdirSync(dirname(manifestPath), { recursive: true });
1593
+ const changedFiles = taskData().changedFilesForTask(projectRoot, taskId, true);
1594
+ writeFileSync(manifestPath, `${changedFiles.join(`
1595
+ `)}
1596
+ `, "utf-8");
1597
+ return manifestPath;
1598
+ }
1599
+ function normalizeChangedFilePath(file) {
1600
+ return file.trim().replace(/\\/g, "/").replace(/^\.\//, "");
1601
+ }
1602
+ function buildStageAddArgs(repoRoot, files, scoped) {
1603
+ if (files.length > 0) {
1604
+ return ["add", "--", ...files];
1605
+ }
1606
+ if (scoped) {
1607
+ return null;
1608
+ }
1609
+ return ["add", "-A", "--", ".", ...stageExcludePathspecs(repoRoot)];
1610
+ }
1611
+ function resolveScopedStageFilesForRepo(projectRoot, repoRoot, taskId, files) {
1612
+ const resolvedManifestFiles = resolveScopedFilesForRepo(projectRoot, repoRoot, files);
1613
+ if (resolvedManifestFiles.length > 0 || !taskId) {
1614
+ return resolvedManifestFiles;
1615
+ }
1616
+ return resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId);
1617
+ }
1618
+ function resolveScopedFilesForRepo(projectRoot, repoRoot, files) {
1619
+ const resolvedFiles = [];
1620
+ const seen = new Set;
1621
+ for (const file of files) {
1622
+ const candidate = resolveScopedRepoPath(repoRoot, file);
1623
+ if (!candidate || seen.has(candidate) || pathResolvesBeyondSymlink(repoRoot, candidate)) {
1624
+ continue;
1625
+ }
1626
+ if (!repoHasPathChange(projectRoot, repoRoot, candidate)) {
1627
+ continue;
1628
+ }
1629
+ seen.add(candidate);
1630
+ resolvedFiles.push(candidate);
1631
+ }
1632
+ return resolvedFiles;
1633
+ }
1634
+ function resolveChangedTaskArtifactFiles(projectRoot, repoRoot, taskId) {
1635
+ const safeTaskId = safePathSegment(taskId, { fallback: "task", maxLength: 96 });
1636
+ const artifactPrefix = `artifacts/${safeTaskId}/`;
1637
+ const resolvedFiles = [];
1638
+ const seen = new Set;
1639
+ for (const file of collectRepoPendingFiles(projectRoot, repoRoot)) {
1640
+ if (!file.startsWith(artifactPrefix)) {
1641
+ continue;
1642
+ }
1643
+ const artifactRelativePath = file.slice(artifactPrefix.length);
1644
+ if (!TASK_ARTIFACT_STAGE_FALLBACK.has(artifactRelativePath)) {
1645
+ continue;
1646
+ }
1647
+ if (seen.has(file) || pathResolvesBeyondSymlink(repoRoot, file)) {
1648
+ continue;
1649
+ }
1650
+ seen.add(file);
1651
+ resolvedFiles.push(file);
1652
+ }
1653
+ return resolvedFiles.sort();
1654
+ }
1655
+ function collectRepoPendingFiles(projectRoot, repoRoot) {
1656
+ const files = new Set;
1657
+ for (const args of [
1658
+ ["diff", "--name-only"],
1659
+ ["diff", "--cached", "--name-only"],
1660
+ ["ls-files", "--others", "--exclude-standard"]
1661
+ ]) {
1662
+ const result = runCapture(gitCmd(projectRoot, repoRoot, ...args), projectRoot);
1663
+ if (result.exitCode !== 0) {
1664
+ continue;
1665
+ }
1666
+ for (const line of result.stdout.split(/\r?\n/)) {
1667
+ const normalized = normalizeChangedFilePath(line);
1668
+ if (!normalized) {
1669
+ continue;
1670
+ }
1671
+ files.add(normalized);
1672
+ }
1673
+ }
1674
+ return [...files].sort();
1675
+ }
1676
+ function resolveScopedRepoPath(repoRoot, file) {
1677
+ const normalized = normalizeChangedFilePath(file);
1678
+ if (!normalized) {
1679
+ return "";
1680
+ }
1681
+ const rules = getScopeRules2();
1682
+ if (rules?.stripPrefixes) {
1683
+ let result = normalized;
1684
+ for (const prefix of rules.stripPrefixes) {
1685
+ if (result.startsWith(prefix)) {
1686
+ result = result.slice(prefix.length);
1687
+ }
1688
+ }
1689
+ return result;
1690
+ }
1691
+ return normalized;
1692
+ }
1693
+ function repoHasPathChange(projectRoot, repoRoot, relativePath) {
1694
+ const result = runCapture(gitCmd(projectRoot, repoRoot, "status", "--short", "--", relativePath), projectRoot);
1695
+ return result.exitCode === 0 && result.stdout.trim().length > 0;
1696
+ }
1697
+ function stageExcludePathspecs(repoRoot) {
1698
+ const patterns = existsSync4(resolve3(repoRoot, ".rig", "task-config.json")) ? [...TASK_RUNTIME_STAGE_EXCLUDES, ...GENERATED_STAGE_EXCLUDES] : [".rig/**", ...GENERATED_STAGE_EXCLUDES];
1699
+ return patterns.map((pattern) => `:(glob,exclude)${pattern}`);
1700
+ }
1701
+ function pathResolvesBeyondSymlink(repoRoot, relativePath) {
1702
+ const parts = relativePath.split("/").filter(Boolean);
1703
+ if (parts.length <= 1) {
1704
+ return false;
1705
+ }
1706
+ let current = repoRoot;
1707
+ for (let index = 0;index < parts.length - 1; index += 1) {
1708
+ current = resolve3(current, parts[index]);
1709
+ try {
1710
+ if (lstatSync(current).isSymbolicLink()) {
1711
+ return true;
1712
+ }
1713
+ } catch {
1714
+ return false;
1715
+ }
1716
+ }
1717
+ return false;
1718
+ }
1719
+ function printRepoStatus(projectRoot, label, repo, expectedBranch) {
1720
+ if (!existsSync4(resolve3(repo, ".git"))) {
1721
+ console.log(`${label}: unavailable (${repo})`);
1722
+ return;
1723
+ }
1724
+ const branch = branchName(projectRoot, repo);
1725
+ const changes = changeCount(projectRoot, repo);
1726
+ console.log(`${label}:`);
1727
+ console.log(` branch: ${branch || "unknown"}`);
1728
+ console.log(` changed files: ${changes}`);
1729
+ if (expectedBranch && label !== "project-rig" && branch !== expectedBranch) {
1730
+ console.log(` warning: branch mismatch (expected ${expectedBranch})`);
1731
+ }
1732
+ }
1733
+ function resolveTaskBranchId(projectRoot, taskId) {
1734
+ if (/^bd-[a-z0-9-]+$/.test(taskId)) {
1735
+ return taskId;
1736
+ }
1737
+ const normalizedTaskId = taskData().lookupTask(projectRoot, taskId);
1738
+ if (normalizedTaskId) {
1739
+ return normalizedTaskId;
1740
+ }
1741
+ const currentTask = taskData().currentTaskId(projectRoot);
1742
+ if (currentTask && currentTask === taskId) {
1743
+ return currentTask;
1744
+ }
1745
+ const runtimeIdFromEnv = (process.env.RIG_TASK_RUNTIME_ID || "").trim();
1746
+ if (runtimeIdFromEnv.startsWith("task-") && runtimeIdFromEnv.length > "task-".length) {
1747
+ return runtimeIdFromEnv.slice("task-".length);
1748
+ }
1749
+ try {
1750
+ const runtimeIdFromContext = loadRuntimeContextFromEnv()?.runtimeId || "";
1751
+ if (runtimeIdFromContext.startsWith("task-") && runtimeIdFromContext.length > "task-".length) {
1752
+ return runtimeIdFromContext.slice("task-".length);
1753
+ }
1754
+ } catch {}
1755
+ const artifactDir = taskData().artifactDirForId(projectRoot, taskId);
1756
+ if (existsSync4(artifactDir)) {
1757
+ return taskId;
1758
+ }
1759
+ throw new Error(`Unknown task id: ${taskId}`);
1760
+ }
1761
+ function branchName(projectRoot, repo) {
1762
+ return runCapture(gitCmd(projectRoot, repo, "rev-parse", "--abbrev-ref", "HEAD"), projectRoot).stdout.trim();
1763
+ }
1764
+ function changeCount(projectRoot, repo) {
1765
+ const status = runCapture(gitCmd(projectRoot, repo, "status", "--short"), projectRoot).stdout.trim();
1766
+ return status ? status.split(/\r?\n/).filter(Boolean).length : 0;
1767
+ }
1768
+ function stagedChangeCount(projectRoot, repo) {
1769
+ const staged = runCapture(gitCmd(projectRoot, repo, "diff", "--cached", "--name-only"), projectRoot).stdout.trim();
1770
+ return staged ? staged.split(/\r?\n/).filter(Boolean).length : 0;
1771
+ }
1772
+ function runOrThrow(projectRoot, command, errorPrefix) {
1773
+ const result = runCapture(command, projectRoot);
1774
+ if (result.exitCode !== 0) {
1775
+ throw new Error(`${errorPrefix}:
1776
+ ${result.stderr || result.stdout}`);
1777
+ }
1778
+ }
1779
+ function runCapture(command, cwd, projectRoot = cwd) {
1780
+ return baseRunCapture(command, cwd, runtimeGitEnv(projectRoot));
1781
+ }
1782
+ function runtimeGitEnv(projectRoot) {
1783
+ const { ctx, runtimeRoot } = resolveRuntimeMetadata(projectRoot);
1784
+ const runtimeHome = runtimeRoot ? resolve3(runtimeRoot, "home") : "";
1785
+ const runtimeTmp = runtimeRoot ? resolve3(runtimeRoot, "tmp") : "";
1786
+ const runtimeCache = runtimeRoot ? resolve3(runtimeRoot, "cache") : "";
1787
+ const runtimeKnownHosts = runtimeHome ? resolve3(runtimeHome, ".ssh", "known_hosts") : "";
1788
+ const runtimeKey = runtimeHome ? resolve3(runtimeHome, ".ssh", "rig-agent-key") : "";
1789
+ const env = {};
1790
+ if (ctx?.workspaceDir) {
1791
+ env.PROJECT_RIG_ROOT = projectRoot;
1792
+ env.RIG_TASK_WORKSPACE = ctx.workspaceDir;
1793
+ env.MONOREPO_ROOT = ctx.workspaceDir;
1794
+ env.MONOREPO_MAIN_ROOT = resolveMonorepoRoot(projectRoot);
1795
+ } else if (projectRoot) {
1796
+ env.PROJECT_RIG_ROOT = projectRoot;
1797
+ }
1798
+ if (runtimeRoot) {
1799
+ env.RIG_RUNTIME_HOME = runtimeRoot;
1800
+ }
1801
+ if (runtimeHome && existsSync4(runtimeHome)) {
1802
+ env.HOME = runtimeHome;
1803
+ env.OPENSSL_CONF = ensureRuntimeOpenSslConfig(runtimeHome);
1804
+ }
1805
+ if (runtimeTmp && existsSync4(runtimeTmp)) {
1806
+ env.TMPDIR = runtimeTmp;
1807
+ }
1808
+ if (runtimeCache && existsSync4(runtimeCache)) {
1809
+ env.XDG_CACHE_HOME = runtimeCache;
1810
+ }
1811
+ const workspaceSecrets = loadDotEnvSecrets(ctx?.workspaceDir || projectRoot, process.env);
1812
+ for (const [key, value] of Object.entries(resolveRuntimeSecrets(process.env, workspaceSecrets))) {
1813
+ if (key === "GITHUB_SSH_KEY" || !value) {
1814
+ continue;
1815
+ }
1816
+ env[key] = value;
1817
+ }
1818
+ const rigGithubToken = process.env.RIG_GITHUB_TOKEN?.trim() || authStateToken(process.env) || "";
1819
+ if (rigGithubToken && !env.GITHUB_TOKEN && !env.GH_TOKEN) {
1820
+ env.GITHUB_TOKEN = rigGithubToken;
1821
+ }
1822
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1823
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1824
+ }
1825
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1826
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1827
+ }
1828
+ if (!env.GREPTILE_GITHUB_TOKEN && env.GITHUB_TOKEN) {
1829
+ env.GREPTILE_GITHUB_TOKEN = env.GITHUB_TOKEN;
1830
+ }
1831
+ const persistedSecrets = loadPersistedRuntimeSecrets(runtimeRoot);
1832
+ for (const [key, value] of Object.entries(persistedSecrets)) {
1833
+ if (!value)
1834
+ continue;
1835
+ if (!env[key]) {
1836
+ env[key] = value;
1837
+ }
1838
+ }
1839
+ if (!env.GITHUB_TOKEN && env.GH_TOKEN) {
1840
+ env.GITHUB_TOKEN = env.GH_TOKEN;
1841
+ }
1842
+ if (!env.GH_TOKEN && env.GITHUB_TOKEN) {
1843
+ env.GH_TOKEN = env.GITHUB_TOKEN;
1844
+ }
1845
+ const gitHubToken = env.GITHUB_TOKEN || env.GH_TOKEN || env.RIG_GITHUB_TOKEN || rigGithubToken;
1846
+ if (gitHubToken) {
1847
+ env.RIG_GITHUB_TOKEN = gitHubToken;
1848
+ env.GITHUB_TOKEN = env.GITHUB_TOKEN || gitHubToken;
1849
+ env.GH_TOKEN = env.GH_TOKEN || gitHubToken;
1850
+ applyGitHubCredentialHelperEnv(env);
1851
+ }
1852
+ if (runtimeKnownHosts && existsSync4(runtimeKnownHosts)) {
1853
+ const sshParts = [
1854
+ "ssh",
1855
+ `-o UserKnownHostsFile="${runtimeKnownHosts}"`,
1856
+ "-o StrictHostKeyChecking=yes",
1857
+ "-F /dev/null"
1858
+ ];
1859
+ if (runtimeKey && existsSync4(runtimeKey)) {
1860
+ sshParts.splice(1, 0, `-i "${runtimeKey}"`, "-o IdentitiesOnly=yes");
1861
+ }
1862
+ env.GIT_SSH_COMMAND = sshParts.join(" ");
1863
+ } else if (process.env.GIT_SSH_COMMAND?.trim()) {
1864
+ env.GIT_SSH_COMMAND = process.env.GIT_SSH_COMMAND;
1865
+ }
1866
+ return Object.keys(env).length > 0 ? env : undefined;
1867
+ }
1868
+ function applyGitHubCredentialHelperEnv(env) {
1869
+ env.GIT_TERMINAL_PROMPT = "0";
1870
+ env.GIT_CONFIG_COUNT = "2";
1871
+ env.GIT_CONFIG_KEY_0 = "credential.helper";
1872
+ env.GIT_CONFIG_VALUE_0 = "";
1873
+ env.GIT_CONFIG_KEY_1 = "credential.helper";
1874
+ 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';
1875
+ }
1876
+ function loadPersistedRuntimeSecrets(runtimeRoot) {
1877
+ if (!runtimeRoot) {
1878
+ return {};
1879
+ }
1880
+ const path = resolve3(runtimeRoot, "runtime-secrets.json");
1881
+ if (!existsSync4(path)) {
1882
+ return {};
1883
+ }
1884
+ try {
1885
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
1886
+ const allowed = new Set(["GITHUB_TOKEN", "GH_TOKEN", "RIG_GITHUB_TOKEN"]);
1887
+ const entries = Object.entries(parsed).filter((entry) => typeof entry[1] === "string" && allowed.has(entry[0]));
1888
+ return Object.fromEntries(entries);
1889
+ } catch {
1890
+ return {};
1891
+ }
1892
+ }
1893
+ function ensureRuntimeOpenSslConfig(runtimeHome) {
1894
+ const sslDir = resolve3(runtimeHome, ".ssl");
1895
+ const sslConfig = resolve3(sslDir, "openssl.cnf");
1896
+ if (!existsSync4(sslDir)) {
1897
+ mkdirSync(sslDir, { recursive: true });
1898
+ }
1899
+ if (!existsSync4(sslConfig)) {
1900
+ writeFileSync(sslConfig, `# Rig runtime OpenSSL config placeholder
1901
+ `);
1902
+ }
1903
+ return sslConfig;
1904
+ }
1905
+ function resolveRuntimeMetadata(projectRoot) {
1906
+ const contextFile = process.env.RIG_RUNTIME_CONTEXT_FILE?.trim();
1907
+ const runtimeHome = process.env.RIG_RUNTIME_HOME?.trim();
1908
+ let ctx = loadRuntimeContextFromEnv();
1909
+ if (runtimeHome) {
1910
+ return {
1911
+ ctx,
1912
+ runtimeRoot: runtimeHome
1913
+ };
1914
+ }
1915
+ if (contextFile) {
1916
+ return {
1917
+ ctx,
1918
+ runtimeRoot: dirname(resolve3(contextFile))
1919
+ };
1920
+ }
1921
+ const inferredContextFile = findRuntimeContextFile(projectRoot);
1922
+ if (existsSync4(inferredContextFile)) {
1923
+ try {
1924
+ ctx = loadRuntimeContext(inferredContextFile);
1925
+ } catch {}
1926
+ return {
1927
+ ctx,
1928
+ runtimeRoot: dirname(inferredContextFile)
1929
+ };
1930
+ }
1931
+ return { ctx, runtimeRoot: "" };
1932
+ }
1933
+ function findRuntimeContextFile(startPath) {
1934
+ let current = resolve3(startPath);
1935
+ while (true) {
1936
+ const candidate = resolve3(current, "runtime-context.json");
1937
+ if (existsSync4(candidate)) {
1938
+ return candidate;
1939
+ }
1940
+ const parent = dirname(current);
1941
+ if (parent === current) {
1942
+ return "";
1943
+ }
1944
+ current = parent;
1945
+ }
1946
+ }
1947
+
1948
+ // packages/cli-surface-plugin/src/control-plane/harness-cli.ts
1949
+ import { setProfile, setReviewProfile, showProfile, showReviewProfile } from "@rig/core/profile-ops";
1950
+ import { resolveCheckoutRoot as resolveMonorepoRoot2 } from "@rig/core/checkout-root";
1951
+ import {
1952
+ COMPLETION_VERIFICATION_CAPABILITY,
1953
+ MEMORY,
1954
+ REPO_OPERATIONS_CAPABILITY,
1955
+ TASK_DATA_SERVICE_CAPABILITY as TASK_DATA_SERVICE_CAPABILITY2,
1956
+ TASK_VERIFY_CAPABILITY
1957
+ } from "@rig/contracts";
1958
+ import { defineCapability as defineCapability2 } from "@rig/core/capability";
1959
+ import { loadCapabilityForRoot, requireInstalledCapability as requireInstalledCapability2 } from "@rig/core/capability-loaders";
1960
+ import { buildPluginHostContext } from "@rig/core/plugin-host-context";
1961
+ import { loadRuntimeContextFromEnv as loadRuntimeContextFromEnv2 } from "@rig/core/runtime-context";
1962
+ var MemoryCap = defineCapability2(MEMORY);
1963
+ var CompletionVerificationCap = defineCapability2(COMPLETION_VERIFICATION_CAPABILITY);
1964
+ var RepoOperationsCap = defineCapability2(REPO_OPERATIONS_CAPABILITY);
1965
+ var TaskVerifyCap = defineCapability2(TASK_VERIFY_CAPABILITY);
1966
+ var TaskDataCap2 = defineCapability2(TASK_DATA_SERVICE_CAPABILITY2);
1967
+ var taskData2 = () => requireInstalledCapability2(TaskDataCap2, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before running task commands.");
1968
+ async function resolveRepoOperations(projectRoot, pluginHostCtx) {
1969
+ const hostCtx = pluginHostCtx ?? await buildPluginHostContext(projectRoot);
1970
+ const repoOps = hostCtx ? await RepoOperationsCap.resolve(hostCtx.pluginHost) : null;
1971
+ if (!repoOps) {
1972
+ throw new Error(`Repo operations require the @rig/repos-plugin plugin in rig.config for project root "${projectRoot}".`);
1973
+ }
1974
+ return repoOps;
1975
+ }
1976
+ async function executeHarnessCommand(projectRoot, args, eventBus, pluginHostCtx, deps) {
1977
+ const [command = "help", ...rest] = args;
1978
+ switch (command) {
1979
+ case "memory": {
1980
+ const runtimeContext = loadRuntimeContextFromEnv2();
1981
+ const taskId = taskData2().currentTaskId(projectRoot) || runtimeContext?.taskId;
1982
+ if (!taskId) {
1983
+ throw new Error("Shared memory commands require an active task context.");
1984
+ }
1985
+ const memoryService = pluginHostCtx ? await MemoryCap.resolve(pluginHostCtx.pluginHost) : await loadCapabilityForRoot(projectRoot, MemoryCap);
1986
+ if (!memoryService) {
1987
+ throw new Error("Shared memory requires the @rig/memory-plugin plugin in rig.config.");
1988
+ }
1989
+ console.log(await memoryService.executeMemoryCommand({
1990
+ projectRoot,
1991
+ taskId,
1992
+ runtimeContext,
1993
+ args: rest,
1994
+ ...eventBus !== undefined ? { eventBus } : {}
1995
+ }));
1996
+ return;
1997
+ }
1998
+ case "info":
1999
+ await taskData2().taskInfo(projectRoot);
2000
+ return;
2001
+ case "scope": {
2002
+ const expand = rest.includes("--files");
2003
+ await taskData2().taskScope(projectRoot, expand);
2004
+ return;
2005
+ }
2006
+ case "deps":
2007
+ await taskData2().taskDeps(projectRoot);
2008
+ return;
2009
+ case "validate": {
2010
+ const ok = await taskData2().taskValidate(projectRoot, undefined, pluginHostCtx?.validatorRegistry ?? undefined);
2011
+ if (!ok) {
2012
+ throw new Error("Validation failed.");
2013
+ }
2014
+ return;
2015
+ }
2016
+ case "completion-verification":
2017
+ case "completition-verification": {
2018
+ const run = pluginHostCtx ? await CompletionVerificationCap.resolve(pluginHostCtx.pluginHost) : null;
2019
+ if (!run) {
2020
+ throw new Error("Completion verification requires the @rig/bundle-default-lifecycle plugin in rig.config.");
2021
+ }
2022
+ const { ok } = await run({ projectRoot });
2023
+ if (!ok) {
2024
+ throw new Error("Completion verification failed.");
2025
+ }
2026
+ return;
2027
+ }
2028
+ case "verify": {
2029
+ const task = rest[0];
2030
+ const capabilityRun = pluginHostCtx ? await TaskVerifyCap.resolve(pluginHostCtx.pluginHost) : null;
2031
+ const verify = deps?.taskVerify ?? (capabilityRun ? (root, taskId) => capabilityRun(taskId ? { projectRoot: root, taskId } : { projectRoot: root }) : undefined);
2032
+ if (!verify) {
2033
+ throw new Error("Task verification requires the @rig/bundle-default-lifecycle plugin in rig.config.");
2034
+ }
2035
+ const ok = await verify(projectRoot, task);
2036
+ if (!ok) {
2037
+ throw new Error("Verification rejected.");
2038
+ }
2039
+ return;
2040
+ }
2041
+ case "status":
2042
+ taskData2().taskStatus(projectRoot);
2043
+ return;
2044
+ case "record": {
2045
+ const type = rest[0];
2046
+ const text = rest.slice(1).join(" ").trim();
2047
+ if (!type || type !== "decision" && type !== "failure" || !text) {
2048
+ throw new Error("Usage: rig-agent record <decision|failure> <text>");
2049
+ }
2050
+ taskData2().taskRecord(projectRoot, type, text);
2051
+ return;
2052
+ }
2053
+ case "lookup": {
2054
+ const id = rest[0];
2055
+ if (!id) {
2056
+ throw new Error("Usage: rig-agent lookup <beads-id>");
2057
+ }
2058
+ console.log(taskData2().taskLookup(projectRoot, id));
2059
+ return;
2060
+ }
2061
+ case "artifacts":
2062
+ taskData2().taskArtifacts(projectRoot);
2063
+ return;
2064
+ case "project-root":
2065
+ console.log(projectRoot);
2066
+ return;
2067
+ case "monorepo-root":
2068
+ console.log(resolveMonorepoRoot2(projectRoot));
2069
+ return;
2070
+ case "git":
2071
+ await executeHarnessGit(projectRoot, rest);
2072
+ return;
2073
+ case "repo":
2074
+ case "repo-sync":
2075
+ await executeHarnessRepo(projectRoot, rest, pluginHostCtx);
2076
+ return;
2077
+ case "profile":
2078
+ await executeHarnessProfile(projectRoot, rest);
2079
+ return;
2080
+ case "review":
2081
+ await executeHarnessReview(projectRoot, rest);
2082
+ return;
2083
+ case "help":
2084
+ case "--help":
2085
+ case "-h":
2086
+ printHelp();
2087
+ return;
2088
+ default:
2089
+ throw new Error(`Unknown command: ${command}`);
2090
+ }
2091
+ }
2092
+ function hasActiveTaskContext(projectRoot) {
2093
+ try {
2094
+ return Boolean(taskData2().currentTaskId(projectRoot));
2095
+ } catch {
2096
+ return false;
2097
+ }
2098
+ }
2099
+ async function executeHarnessGit(projectRoot, args) {
2100
+ const [command = "status", ...rest] = args;
2101
+ switch (command) {
2102
+ case "status": {
2103
+ const task = takeOption2(rest, "--task");
2104
+ gitStatus(projectRoot, task.value);
2105
+ return;
2106
+ }
2107
+ case "changed": {
2108
+ const task = takeOption2(rest, "--task");
2109
+ const scoped = task.rest.includes("--scoped");
2110
+ const files = gitChanged(projectRoot, task.value, scoped);
2111
+ console.log(files.length > 0 ? files.join(`
2112
+ `) : "(none)");
2113
+ return;
2114
+ }
2115
+ case "preflight": {
2116
+ const task = takeOption2(rest, "--task");
2117
+ const strict = task.rest.includes("--strict");
2118
+ const ok = gitPreflight(projectRoot, task.value, strict);
2119
+ if (!ok) {
2120
+ throw new Error("Git preflight failed.");
2121
+ }
2122
+ return;
2123
+ }
2124
+ case "sync-branch":
2125
+ case "ensure-branch": {
2126
+ const task = takeOption2(rest, "--task");
2127
+ gitSyncBranch(projectRoot, task.value);
2128
+ return;
2129
+ }
2130
+ case "commit": {
2131
+ const task = takeOption2(rest, "--task");
2132
+ const target = takeOption2(task.rest, "--target");
2133
+ const message = takeOption2(target.rest, "--message");
2134
+ const allowEmpty = message.rest.includes("--allow-empty");
2135
+ const scoped = shouldScopeGitCommit(message.rest, Boolean(task.value) || hasActiveTaskContext(projectRoot));
2136
+ gitCommit({
2137
+ projectRoot,
2138
+ ...task.value !== undefined ? { taskId: task.value } : {},
2139
+ target: target.value || "monorepo",
2140
+ ...message.value !== undefined ? { message: message.value } : {},
2141
+ allowEmpty,
2142
+ scoped
2143
+ });
2144
+ return;
2145
+ }
2146
+ case "snapshot": {
2147
+ const task = takeOption2(rest, "--task");
2148
+ const output = takeOption2(task.rest, "--output");
2149
+ const file = gitSnapshot(projectRoot, task.value, output.value);
2150
+ console.log(`Snapshot written: ${file}`);
2151
+ return;
2152
+ }
2153
+ case "open-pr": {
2154
+ const task = takeOption2(rest, "--task");
2155
+ const target = takeOption2(task.rest, "--target");
2156
+ const reviewer = takeOption2(target.rest, "--reviewer");
2157
+ const base = takeOption2(reviewer.rest, "--base");
2158
+ const title = takeOption2(base.rest, "--title");
2159
+ const body = takeOption2(title.rest, "--body");
2160
+ const draft = body.rest.includes("--draft");
2161
+ const result = gitOpenPr({
2162
+ projectRoot,
2163
+ ...task.value !== undefined ? { taskId: task.value } : {},
2164
+ ...target.value !== undefined ? { target: target.value } : {},
2165
+ ...reviewer.value !== undefined ? { reviewer: reviewer.value } : {},
2166
+ ...base.value !== undefined ? { base: base.value } : {},
2167
+ ...title.value !== undefined ? { title: title.value } : {},
2168
+ ...body.value !== undefined ? { body: body.value } : {},
2169
+ draft
2170
+ });
2171
+ console.log(`PR ready (${result.repoLabel}): ${result.url}`);
2172
+ if (result.reviewer && result.reviewerSource) {
2173
+ console.log(`Reviewer assigned: ${result.reviewer} (${result.reviewerSource})`);
2174
+ } else if (result.reviewer) {
2175
+ console.log(`Reviewer assigned: ${result.reviewer}`);
2176
+ }
2177
+ return;
2178
+ }
2179
+ default:
2180
+ throw new Error(`Unknown harness git command: ${command}`);
2181
+ }
2182
+ }
2183
+ async function executeHarnessRepo(projectRoot, args, pluginHostCtx) {
2184
+ const [command = "ensure", ...rest] = args;
2185
+ const task = takeOption2(rest, "--task");
2186
+ const repoOps = await resolveRepoOperations(projectRoot, pluginHostCtx);
2187
+ switch (command) {
2188
+ case "sync":
2189
+ case "ensure":
2190
+ repoOps.repoEnsure(projectRoot, task.value);
2191
+ return;
2192
+ case "pins": {
2193
+ const pins = repoOps.repoPins(projectRoot, task.value);
2194
+ printPins(pins);
2195
+ return;
2196
+ }
2197
+ case "verify": {
2198
+ const ok = repoOps.repoVerify(projectRoot, task.value);
2199
+ if (!ok) {
2200
+ throw new Error("Repo pin verification failed.");
2201
+ }
2202
+ return;
2203
+ }
2204
+ case "discover": {
2205
+ const pins = repoOps.repoDiscover(projectRoot, task.value);
2206
+ printPins(pins);
2207
+ return;
2208
+ }
2209
+ case "baseline": {
2210
+ const refresh = task.rest.includes("--refresh");
2211
+ const pins = repoOps.repoBaseline(projectRoot, refresh);
2212
+ printPins(pins);
2213
+ return;
2214
+ }
2215
+ default:
2216
+ throw new Error(`Unknown harness repo command: ${command}`);
2217
+ }
2218
+ }
2219
+ async function executeHarnessProfile(projectRoot, args) {
2220
+ const [command = "show", ...rest] = args;
2221
+ if (command === "show") {
2222
+ await showProfile(projectRoot, rest.includes("--compact"));
2223
+ return;
2224
+ }
2225
+ if (command === "set") {
2226
+ const first = rest[0];
2227
+ if (first && first !== "pi" && !first.startsWith("--")) {
2228
+ throw new Error("Only the Pi runtime profile is supported by this Rig substrate.");
2229
+ }
2230
+ const model = takeOption2(rest, "--model");
2231
+ const runtime = takeOption2(model.rest, "--runtime");
2232
+ const plugin = takeOption2(runtime.rest, "--plugin");
2233
+ await setProfile(projectRoot, {
2234
+ ...model.value !== undefined ? { model: model.value } : {},
2235
+ ...runtime.value !== undefined ? { runtime: runtime.value } : {},
2236
+ ...plugin.value !== undefined ? { plugin: plugin.value } : {}
2237
+ });
2238
+ return;
2239
+ }
2240
+ throw new Error(`Unknown harness profile command: ${command}`);
2241
+ }
2242
+ async function executeHarnessReview(projectRoot, args) {
2243
+ const [command = "show", ...rest] = args;
2244
+ if (command === "show") {
2245
+ await showReviewProfile(projectRoot);
2246
+ return;
2247
+ }
2248
+ if (command === "set") {
2249
+ const mode = rest[0];
2250
+ if (!mode) {
2251
+ throw new Error("Usage: rig-agent review set <off|advisory|required> [--provider github|greptile]");
2252
+ }
2253
+ const provider = takeOption2(rest.slice(1), "--provider");
2254
+ await setReviewProfile(projectRoot, mode, provider.value);
2255
+ return;
2256
+ }
2257
+ throw new Error(`Unknown harness review command: ${command}`);
2258
+ }
2259
+ function printPins(pins) {
2260
+ if (Object.keys(pins).length === 0) {
2261
+ console.log("(none)");
2262
+ return;
2263
+ }
2264
+ for (const [key, value] of Object.entries(pins)) {
2265
+ console.log(`${key} ${value}`);
2266
+ }
2267
+ }
2268
+ function takeOption2(args, option) {
33
2269
  const rest = [];
34
2270
  let value;
35
- for (let index = 0;index < args.length; index += 1) {
36
- const current = args[index];
2271
+ for (let i = 0;i < args.length; i += 1) {
2272
+ const current = args[i];
37
2273
  if (current === option) {
38
- const next = args[index + 1];
2274
+ const next = args[i + 1];
39
2275
  if (!next || next.startsWith("-")) {
40
- throw new CliError(`Missing value for ${option}`, 1, { hint: `Provide a value after ${option}, e.g. \`${option} <value>\`.` });
2276
+ throw new Error(`Missing value for ${option}`);
41
2277
  }
42
2278
  value = next;
43
- index += 1;
2279
+ i += 1;
44
2280
  continue;
45
2281
  }
46
2282
  if (current !== undefined) {
@@ -49,17 +2285,40 @@ function takeOption(args, option) {
49
2285
  }
50
2286
  return { value, rest };
51
2287
  }
52
- function requireNoExtraArgs(args, usage) {
53
- if (args.length > 0) {
54
- throw new CliError(`Unexpected arguments: ${args.join(" ")}
55
- Usage: ${usage}`);
56
- }
2288
+ function printHelp() {
2289
+ console.log(`rig-agent \u2014 Agent CLI for Project Rig
2290
+
2291
+ ` + `NOTE: This is the legacy bash shim. Agents should use rig-agent.
2292
+
2293
+ ` + `ORIENTATION:
2294
+ ` + ` rig-agent info Your task, role, scope, deps
2295
+ ` + ` rig-agent scope [--files] Scope globs; --files expands to file list
2296
+ ` + ` rig-agent deps Read dependency artifacts (decisions, next-actions)
2297
+ ` + ` rig-agent lookup <id> Validate a beads task ID
2298
+ ` + ` rig-agent status Epic/task progress
2299
+
2300
+ ` + `EXECUTION:
2301
+ ` + ` rig-agent validate Run validation commands for your task
2302
+ ` + ` rig-agent completion-verification Run final validation/review gate
2303
+ ` + ` rig-agent record decision "..."
2304
+ ` + ` rig-agent record failure "..."
2305
+ ` + ` rig-agent git ... Task-aware git helper
2306
+ ` + ` rig-agent repo sync Ensure monorepo checkout + task repo pins
2307
+ ` + ` rig-agent profile ... Show/set runtime profile
2308
+ ` + ` rig-agent review ... Show/set AI review profile
2309
+
2310
+ ` + `COMPLETION:
2311
+ ` + ` rig-agent artifacts Scaffold completion artifacts
2312
+ ` + ` rig-agent artifact-dir Print absolute artifact directory path
2313
+ ` + ` rig-agent project-root Print absolute task worktree root
2314
+ ` + ` rig-agent monorepo-root Print absolute monorepo root in the task worktree
2315
+ ` + ` rig-agent artifact-write <f> Write artifact from stdin`);
57
2316
  }
58
2317
 
59
2318
  // packages/cli-surface-plugin/src/commands/repo-git-harness.ts
60
- import { executeHarnessCommand } from "@rig/runtime/control-plane/native/harness-cli";
61
- import { repoEnsure, resetBaseline } from "@rig/runtime/control-plane/native/repo-ops";
62
- import { taskVerify } from "@rig/bundle-default-lifecycle/control-plane/task-verify";
2319
+ import { REPO_OPERATIONS_CAPABILITY as REPO_OPERATIONS_CAPABILITY2 } from "@rig/contracts";
2320
+ import { defineCapability as defineCapability3 } from "@rig/core/capability";
2321
+ import { buildPluginHostContext as buildPluginHostContext2 } from "@rig/core/plugin-host-context";
63
2322
 
64
2323
  // packages/cli-surface-plugin/src/withMutedConsole.ts
65
2324
  function isPromise(value) {
@@ -101,30 +2360,30 @@ function withMutedConsole(mute, fn) {
101
2360
  }
102
2361
 
103
2362
  // packages/cli-surface-plugin/src/commands/_policy.ts
104
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
105
- import { resolve as resolve2 } from "path";
2363
+ import { appendFileSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4 } from "fs";
2364
+ import { resolve as resolve5 } from "path";
106
2365
 
107
2366
  // packages/cli-surface-plugin/src/commands/_paths.ts
108
- import { resolve } from "path";
109
- import { resolveMonorepoRoot } from "@rig/runtime/layout";
2367
+ import { resolve as resolve4 } from "path";
2368
+ import { resolveMonorepoRoot as resolveMonorepoRoot3 } from "@rig/core/layout";
110
2369
  function resolveControlPlaneHostStateRoot(projectRoot) {
111
- return resolve(projectRoot, ".rig");
2370
+ return resolve4(projectRoot, ".rig");
112
2371
  }
113
2372
  function resolveControlPlaneHostLogsDir(projectRoot) {
114
- return resolve(resolveControlPlaneHostStateRoot(projectRoot), "logs");
2373
+ return resolve4(resolveControlPlaneHostStateRoot(projectRoot), "logs");
115
2374
  }
116
2375
  function resolveControlPlaneDefinitionRoot(projectRoot) {
117
- return resolve(projectRoot, "rig");
2376
+ return resolve4(projectRoot, "rig");
118
2377
  }
119
2378
 
120
2379
  // packages/cli-surface-plugin/src/commands/_policy.ts
121
2380
  function loadPolicyFile(projectRoot) {
122
- const policyPath = resolve2(resolveControlPlaneDefinitionRoot(projectRoot), "policy", "policy.json");
123
- if (!existsSync(policyPath)) {
2381
+ const policyPath = resolve5(resolveControlPlaneDefinitionRoot(projectRoot), "policy", "policy.json");
2382
+ if (!existsSync5(policyPath)) {
124
2383
  return { mode: "observe", rules: [] };
125
2384
  }
126
2385
  try {
127
- const parsed = JSON.parse(readFileSync(policyPath, "utf8"));
2386
+ const parsed = JSON.parse(readFileSync4(policyPath, "utf8"));
128
2387
  const mode = parsed.mode === "off" || parsed.mode === "observe" || parsed.mode === "enforce" ? parsed.mode : "observe";
129
2388
  const rules = Array.isArray(parsed.rules) ? parsed.rules.filter((value) => Boolean(value && typeof value === "object" && !Array.isArray(value))).map((value, index) => {
130
2389
  const rule = {
@@ -172,8 +2431,8 @@ function appendControlledBashAudit(context, mode, command, args, matchedRuleIds,
172
2431
  return;
173
2432
  }
174
2433
  const logsDir = resolveControlPlaneHostLogsDir(context.projectRoot);
175
- const logFile = resolve2(logsDir, "controlled-bash.jsonl");
176
- mkdirSync(logsDir, { recursive: true });
2434
+ const logFile = resolve5(logsDir, "controlled-bash.jsonl");
2435
+ mkdirSync2(logsDir, { recursive: true });
177
2436
  const payload = {
178
2437
  timestamp: new Date().toISOString(),
179
2438
  mode,
@@ -215,19 +2474,34 @@ async function enforceNativeCommandPolicy(context, args, options) {
215
2474
  }
216
2475
 
217
2476
  // packages/cli-surface-plugin/src/commands/repo-git-harness.ts
2477
+ var RepoOperationsCap2 = defineCapability3(REPO_OPERATIONS_CAPABILITY2);
2478
+ async function resolveRepoOperations2(projectRoot) {
2479
+ const hostCtx = await buildPluginHostContext2(projectRoot);
2480
+ const repoOps = hostCtx ? await RepoOperationsCap2.resolve(hostCtx.pluginHost) : null;
2481
+ if (!repoOps) {
2482
+ throw new CliError(`No REPO_OPERATIONS capability is registered for project root "${projectRoot}".`, 2, { hint: "Install @rig/repos-plugin (it ships in the default bundle) before running repo operations." });
2483
+ }
2484
+ return repoOps;
2485
+ }
218
2486
  async function executeRepo(context, args) {
219
2487
  const [command = "sync", ...rest] = args;
220
2488
  switch (command) {
221
2489
  case "sync": {
222
2490
  const { value: task, rest: remaining } = takeOption(rest, "--task");
223
2491
  requireNoExtraArgs(remaining, "rig repo sync [--task <task-id>]");
224
- withMutedConsole(context.outputMode === "json", () => repoEnsure(context.projectRoot, task || undefined));
2492
+ await withMutedConsole(context.outputMode === "json", async () => {
2493
+ const repoOps = await resolveRepoOperations2(context.projectRoot);
2494
+ repoOps.repoEnsure(context.projectRoot, task || undefined);
2495
+ });
225
2496
  return { ok: true, group: "repo", command, details: { task: task || null } };
226
2497
  }
227
2498
  case "reset-baseline": {
228
2499
  const { value: keepTaskStatusFlag, rest: remaining } = takeFlag(rest, "--keep-task-status");
229
2500
  requireNoExtraArgs(remaining, "rig repo reset-baseline [--keep-task-status]");
230
- withMutedConsole(context.outputMode === "json", () => resetBaseline(context.projectRoot, keepTaskStatusFlag));
2501
+ await withMutedConsole(context.outputMode === "json", async () => {
2502
+ const repoOps = await resolveRepoOperations2(context.projectRoot);
2503
+ repoOps.resetBaseline(context.projectRoot, keepTaskStatusFlag);
2504
+ });
231
2505
  return { ok: true, group: "repo", command, details: { keepTaskStatus: keepTaskStatusFlag } };
232
2506
  }
233
2507
  default:
@@ -270,7 +2544,7 @@ async function executeHarness(context, args) {
270
2544
  return { ok: true, group: "harness", command: args[0] ?? "harness" };
271
2545
  }
272
2546
  try {
273
- await executeHarnessCommand(context.projectRoot, args, undefined, undefined, { taskVerify });
2547
+ await executeHarnessCommand(context.projectRoot, args);
274
2548
  } catch (error) {
275
2549
  throw new CliError(error instanceof Error ? error.message : String(error), 2);
276
2550
  }