@h-rig/cli-surface-plugin 0.0.6-alpha.157 → 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,17 +1,656 @@
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();
14
- }
653
+ super(message, exitCode, options);
15
654
  }
16
655
  }
17
656
  function takeOption(args, option) {
@@ -41,13 +680,639 @@ Usage: ${usage}`);
41
680
  }
42
681
  }
43
682
 
44
- // packages/cli-surface-plugin/src/commands/workspace.ts
683
+ // packages/cli-surface-plugin/src/control-plane/workspace-ops.ts
684
+ import { spawn } from "child_process";
45
685
  import {
46
- mutateWorkspaceServiceFabric,
47
- readWorkspaceRemoteFleet,
48
- readWorkspaceSummary,
49
- readWorkspaceTopology
50
- } from "@rig/runtime/control-plane/native/workspace-ops";
686
+ appendFileSync,
687
+ closeSync,
688
+ existsSync as existsSync2,
689
+ mkdirSync,
690
+ openSync,
691
+ readdirSync,
692
+ readFileSync as readFileSync2,
693
+ writeFileSync
694
+ } from "fs";
695
+ import { join, resolve as resolve2 } from "path";
696
+ import { TASK_DATA_SERVICE_CAPABILITY } from "@rig/contracts";
697
+ import { defineCapability } from "@rig/core/capability";
698
+ import { requireInstalledCapability } from "@rig/core/capability-loaders";
699
+ var TaskDataCap = defineCapability(TASK_DATA_SERVICE_CAPABILITY);
700
+ var taskData = () => requireInstalledCapability(TaskDataCap, "task-data capability unavailable: load @rig/task-sources-plugin (default bundle) before reading workspace task counts.");
701
+ var ONLINE_REMOTE_HOST_STATUSES = new Set(["ready", "busy", "degraded", "draining"]);
702
+ function asRecord(value) {
703
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
704
+ }
705
+ function asString(value) {
706
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
707
+ }
708
+ function asStringArray(value) {
709
+ return Array.isArray(value) ? value.flatMap((entry) => typeof entry === "string" && entry.trim().length > 0 ? [entry.trim()] : []) : [];
710
+ }
711
+ function asStringMap(value) {
712
+ const record = asRecord(value);
713
+ if (!record) {
714
+ return {};
715
+ }
716
+ const entries = Object.entries(record).flatMap(([key, entry]) => {
717
+ if (typeof entry === "string" && entry.trim().length > 0) {
718
+ return [[key, entry]];
719
+ }
720
+ if (typeof entry === "number" || typeof entry === "boolean") {
721
+ return [[key, String(entry)]];
722
+ }
723
+ return [];
724
+ });
725
+ return Object.fromEntries(entries);
726
+ }
727
+ function parseScalar(raw) {
728
+ const value = raw.trim();
729
+ if (value.length === 0)
730
+ return "";
731
+ if (value === "null")
732
+ return null;
733
+ if (value === "true")
734
+ return true;
735
+ if (value === "false")
736
+ return false;
737
+ if (/^-?\d+$/.test(value))
738
+ return Number.parseInt(value, 10);
739
+ if (/^-?\d+\.\d+$/.test(value))
740
+ return Number.parseFloat(value);
741
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
742
+ return value.slice(1, -1);
743
+ }
744
+ return value;
745
+ }
746
+ function parseYamlSubset(source) {
747
+ const lines = source.split(/\r?\n/).map((line) => line.replace(/\t/g, " ")).filter((line) => line.trim().length > 0 && !line.trimStart().startsWith("#"));
748
+ const root = {};
749
+ const stack = [
750
+ { indent: -1, value: root }
751
+ ];
752
+ const ensureContainer = (indent) => {
753
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
754
+ stack.pop();
755
+ }
756
+ return stack[stack.length - 1];
757
+ };
758
+ for (let index = 0;index < lines.length; index += 1) {
759
+ const rawLine = lines[index];
760
+ const indent = rawLine.match(/^ */)?.[0].length ?? 0;
761
+ const line = rawLine.trim();
762
+ const parent = ensureContainer(indent);
763
+ if (line.startsWith("- ")) {
764
+ const itemContent = line.slice(2);
765
+ if (!Array.isArray(parent.value)) {
766
+ continue;
767
+ }
768
+ if (itemContent.includes(":")) {
769
+ const [firstKey, ...rest] = itemContent.split(":");
770
+ const valueText2 = rest.join(":").trim();
771
+ const entry = {};
772
+ entry[firstKey.trim()] = valueText2.length > 0 ? parseScalar(valueText2) : "";
773
+ parent.value.push(entry);
774
+ stack.push({ indent, value: entry });
775
+ } else {
776
+ parent.value.push(parseScalar(itemContent));
777
+ }
778
+ continue;
779
+ }
780
+ const colonIndex = line.indexOf(":");
781
+ if (colonIndex === -1 || !asRecord(parent.value)) {
782
+ continue;
783
+ }
784
+ const key = line.slice(0, colonIndex).trim();
785
+ const valueText = line.slice(colonIndex + 1).trim();
786
+ const parentRecord = parent.value;
787
+ if (valueText.length > 0) {
788
+ parentRecord[key] = parseScalar(valueText);
789
+ continue;
790
+ }
791
+ const nextLine = lines[index + 1]?.trim() ?? "";
792
+ const container = nextLine.startsWith("- ") ? [] : {};
793
+ parentRecord[key] = container;
794
+ stack.push({ indent, value: container });
795
+ }
796
+ return root;
797
+ }
798
+ function relativePath(rootPath, targetPath) {
799
+ return targetPath.startsWith(rootPath) ? targetPath.slice(rootPath.length + 1) || "." : targetPath;
800
+ }
801
+ function resolveRuntimeWorkspaceRoot(projectRoot) {
802
+ return resolve2(process.env.RIG_TASK_WORKSPACE?.trim() || projectRoot);
803
+ }
804
+ function resolveServiceFabricPaths(projectRoot) {
805
+ const workspaceRoot = resolveRuntimeWorkspaceRoot(projectRoot);
806
+ const fabricRoot = resolve2(workspaceRoot, ".rig", "service-fabric");
807
+ return {
808
+ workspaceRoot,
809
+ fabricRoot,
810
+ statePath: resolve2(fabricRoot, "fabric-state.json"),
811
+ logsDir: resolve2(fabricRoot, "logs")
812
+ };
813
+ }
814
+ function issueStatus(status) {
815
+ switch (status) {
816
+ case "ready":
817
+ case "open":
818
+ case "queued":
819
+ case "blocked":
820
+ case "completed":
821
+ case "draft":
822
+ case "cancelled":
823
+ return status;
824
+ case "in_progress":
825
+ case "under_review":
826
+ return "running";
827
+ case "unknown":
828
+ return "unknown";
829
+ }
830
+ }
831
+ function collectTaskCounts(projectRoot) {
832
+ const initial = {
833
+ total: 0,
834
+ ready: 0,
835
+ open: 0,
836
+ queued: 0,
837
+ running: 0,
838
+ blocked: 0,
839
+ failed: 0,
840
+ unknown: 0,
841
+ completed: 0,
842
+ draft: 0,
843
+ cancelled: 0
844
+ };
845
+ const snapshot = taskData().readSyncedTrackerState(projectRoot, {}, { allowLocalFallback: true });
846
+ for (const issue of snapshot.issues) {
847
+ if (!issue.id.startsWith("bd-"))
848
+ continue;
849
+ if (issue.issueType === "epic")
850
+ continue;
851
+ initial.total += 1;
852
+ initial[issueStatus(issue.status)] += 1;
853
+ }
854
+ return initial;
855
+ }
856
+ function listManifestDirs(rootPath) {
857
+ const candidates = [join(rootPath, "microservices")];
858
+ const discovered = new Set;
859
+ for (const candidate of candidates) {
860
+ if (!existsSync2(candidate))
861
+ continue;
862
+ for (const entry of readdirSync(candidate, { withFileTypes: true })) {
863
+ if (!entry.isDirectory() || entry.name.startsWith("_"))
864
+ continue;
865
+ const serviceDir = join(candidate, entry.name);
866
+ if (existsSync2(join(serviceDir, "service.yaml"))) {
867
+ discovered.add(serviceDir);
868
+ }
869
+ }
870
+ }
871
+ return [...discovered].sort((left, right) => left.localeCompare(right));
872
+ }
873
+ function parsePortValue(value) {
874
+ if (typeof value === "number" && Number.isFinite(value)) {
875
+ return Number(value);
876
+ }
877
+ if (typeof value === "string") {
878
+ const parsed = Number(value);
879
+ if (Number.isFinite(parsed)) {
880
+ return parsed;
881
+ }
882
+ }
883
+ return 0;
884
+ }
885
+ function resolveMonorepoWorkspaceRoot(projectRoot) {
886
+ return resolve2(process.env.MONOREPO_ROOT?.trim() || projectRoot);
887
+ }
888
+ function resolveLocalWorkingDir(monorepoRoot, serviceDir, rawWorkingDir) {
889
+ if (!rawWorkingDir) {
890
+ return serviceDir;
891
+ }
892
+ if (rawWorkingDir.startsWith("/")) {
893
+ return rawWorkingDir;
894
+ }
895
+ if (rawWorkingDir.startsWith(".")) {
896
+ return resolve2(serviceDir, rawWorkingDir);
897
+ }
898
+ return resolve2(monorepoRoot, rawWorkingDir);
899
+ }
900
+ function readNativeServicePlans(projectRoot, selectedServices = []) {
901
+ const manifestDirs = listManifestDirs(projectRoot);
902
+ const warnings = [];
903
+ const services = [];
904
+ const monorepoRoot = resolveMonorepoWorkspaceRoot(projectRoot);
905
+ for (const serviceDir of manifestDirs) {
906
+ const manifestPath = join(serviceDir, "service.yaml");
907
+ try {
908
+ const parsed = asRecord(parseYamlSubset(readFileSync2(manifestPath, "utf-8")));
909
+ const name = asString(parsed?.name) ?? serviceDir.split("/").at(-1) ?? "service";
910
+ if (selectedServices.length > 0 && !selectedServices.includes(name)) {
911
+ continue;
912
+ }
913
+ const localDev = asRecord(parsed?.local_dev);
914
+ const runtime = asString(parsed?.runtime) ?? "unknown";
915
+ const healthcheck = asString(parsed?.healthcheck) ?? "/health";
916
+ const routePrefix = asString(localDev?.route_prefix);
917
+ const command = asString(localDev?.command);
918
+ const requestedMode = asString(localDev?.mode);
919
+ const environment = asStringMap(localDev?.env);
920
+ const port = parsePortValue(environment.PORT) || parsePortValue(environment.HTTP_PORT) || parsePortValue(parsed?.port) || 0;
921
+ const workingDir = resolveLocalWorkingDir(monorepoRoot, serviceDir, asString(localDev?.working_dir));
922
+ const readinessPath = asString(localDev?.readiness_path) ?? healthcheck;
923
+ let mode = requestedMode === "process" || requestedMode !== "stub" && command ? "process" : "stub";
924
+ if (requestedMode === "proxy" && !command) {
925
+ warnings.push(`${relativePath(projectRoot, manifestPath)} declares local_dev.mode=proxy without a command; treating it as a stub`);
926
+ mode = "stub";
927
+ }
928
+ if (mode === "process" && !command) {
929
+ warnings.push(`${relativePath(projectRoot, manifestPath)} declares process local_dev mode without a command; treating it as a stub`);
930
+ mode = "stub";
931
+ }
932
+ services.push({
933
+ name,
934
+ relativeRootPath: relativePath(projectRoot, serviceDir),
935
+ absoluteRootPath: serviceDir,
936
+ runtime,
937
+ port,
938
+ healthcheck,
939
+ routePrefix,
940
+ mode,
941
+ command: mode === "process" ? command : null,
942
+ workingDir,
943
+ environment,
944
+ readinessPath
945
+ });
946
+ } catch (error) {
947
+ warnings.push(`Failed to parse ${relativePath(projectRoot, manifestPath)}: ${error instanceof Error ? error.message : String(error)}`);
948
+ }
949
+ }
950
+ return { services, warnings };
951
+ }
952
+ function readPersistedFabricState(projectRoot) {
953
+ const { statePath } = resolveServiceFabricPaths(projectRoot);
954
+ if (!existsSync2(statePath)) {
955
+ return null;
956
+ }
957
+ try {
958
+ return JSON.parse(readFileSync2(statePath, "utf-8"));
959
+ } catch {
960
+ return null;
961
+ }
962
+ }
963
+ function writePersistedFabricState(projectRoot, state) {
964
+ const { fabricRoot, logsDir, statePath } = resolveServiceFabricPaths(projectRoot);
965
+ mkdirSync(fabricRoot, { recursive: true });
966
+ mkdirSync(logsDir, { recursive: true });
967
+ writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
968
+ `, "utf-8");
969
+ }
970
+ function isProcessRunning(pid) {
971
+ if (!pid || pid <= 0) {
972
+ return false;
973
+ }
974
+ try {
975
+ process.kill(pid, 0);
976
+ return true;
977
+ } catch {
978
+ return false;
979
+ }
980
+ }
981
+ async function stopProcess(pid) {
982
+ if (!pid || pid <= 0) {
983
+ return;
984
+ }
985
+ try {
986
+ process.kill(pid, "SIGTERM");
987
+ } catch {
988
+ return;
989
+ }
990
+ const deadline = Date.now() + 5000;
991
+ while (Date.now() < deadline) {
992
+ if (!isProcessRunning(pid)) {
993
+ return;
994
+ }
995
+ await Bun.sleep(100);
996
+ }
997
+ try {
998
+ process.kill(pid, "SIGKILL");
999
+ } catch {}
1000
+ }
1001
+ async function probeReadiness(service, timeoutMs = 1000) {
1002
+ if (service.mode === "stub") {
1003
+ return true;
1004
+ }
1005
+ if (!isProcessRunning(service.pid) || service.port <= 0) {
1006
+ return false;
1007
+ }
1008
+ const controller = new AbortController;
1009
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1010
+ try {
1011
+ const response = await fetch(`http://127.0.0.1:${service.port}${service.readinessPath}`, {
1012
+ signal: controller.signal
1013
+ });
1014
+ return response.ok;
1015
+ } catch {
1016
+ return false;
1017
+ } finally {
1018
+ clearTimeout(timeout);
1019
+ }
1020
+ }
1021
+ async function waitForServiceHealthy(service, timeoutMs = 30000) {
1022
+ if (service.mode === "stub") {
1023
+ return true;
1024
+ }
1025
+ const deadline = Date.now() + timeoutMs;
1026
+ while (Date.now() < deadline) {
1027
+ if (await probeReadiness(service, 1000)) {
1028
+ return true;
1029
+ }
1030
+ if (!isProcessRunning(service.pid)) {
1031
+ return false;
1032
+ }
1033
+ await Bun.sleep(250);
1034
+ }
1035
+ return false;
1036
+ }
1037
+ function materializeStateFromPlans(projectRoot, plans, persisted) {
1038
+ const persistedByName = new Map((persisted?.services ?? []).map((service) => [service.name, service]));
1039
+ const { logsDir } = resolveServiceFabricPaths(projectRoot);
1040
+ return plans.map((plan) => {
1041
+ const previous = persistedByName.get(plan.name);
1042
+ return {
1043
+ ...plan,
1044
+ pid: previous?.pid ?? null,
1045
+ logPath: previous?.logPath ?? resolve2(logsDir, `${plan.name}.log`),
1046
+ status: previous?.status ?? "stopped"
1047
+ };
1048
+ });
1049
+ }
1050
+ function summarizeFabricState(projectRoot, services, warnings, updatedAt) {
1051
+ const { fabricRoot } = resolveServiceFabricPaths(projectRoot);
1052
+ const healthyServiceCount = services.filter((service) => service.status === "healthy").length;
1053
+ const status = services.length === 0 ? "empty" : services.every((service) => service.status === "stopped") ? "stopped" : services.every((service) => service.status === "healthy") ? "ready" : services.some((service) => service.status === "booting") ? "booting" : "degraded";
1054
+ return {
1055
+ updatedAt,
1056
+ status,
1057
+ serviceCount: services.length,
1058
+ healthyServiceCount,
1059
+ stateDir: fabricRoot,
1060
+ services: services.map((service) => ({
1061
+ name: service.name,
1062
+ relativeRootPath: service.relativeRootPath,
1063
+ absoluteRootPath: service.absoluteRootPath,
1064
+ mode: service.mode,
1065
+ port: service.port,
1066
+ healthcheck: service.healthcheck,
1067
+ routePrefix: service.routePrefix,
1068
+ command: service.command,
1069
+ environment: service.environment,
1070
+ status: service.status,
1071
+ pid: service.pid,
1072
+ logPath: service.logPath
1073
+ })),
1074
+ warnings
1075
+ };
1076
+ }
1077
+ function readWorkspaceTopology(projectRoot, compiledAt = new Date().toISOString()) {
1078
+ const manifestDirs = listManifestDirs(projectRoot);
1079
+ const warnings = [];
1080
+ const services = [];
1081
+ for (const serviceDir of manifestDirs) {
1082
+ const manifestPath = join(serviceDir, "service.yaml");
1083
+ try {
1084
+ const parsed = asRecord(parseYamlSubset(readFileSync2(manifestPath, "utf-8")));
1085
+ const name = asString(parsed?.name) ?? serviceDir.split("/").at(-1) ?? "service";
1086
+ const runtime = asString(parsed?.runtime) ?? "unknown";
1087
+ const portValue = typeof parsed?.port === "number" ? parsed.port : typeof parsed?.port === "string" ? Number(parsed.port) : NaN;
1088
+ services.push({
1089
+ name,
1090
+ relativeRootPath: relativePath(projectRoot, serviceDir),
1091
+ runtime,
1092
+ port: Number.isFinite(portValue) ? Number(portValue) : null,
1093
+ healthcheck: asString(parsed?.healthcheck),
1094
+ splitReadyChecklist: asStringArray(parsed?.split_ready_checklist)
1095
+ });
1096
+ } catch (error) {
1097
+ warnings.push(`Failed to parse ${relativePath(projectRoot, manifestPath)}: ${error instanceof Error ? error.message : String(error)}`);
1098
+ }
1099
+ }
1100
+ return {
1101
+ compiledAt,
1102
+ status: services.length === 0 ? "empty" : warnings.length > 0 ? "degraded" : "ready",
1103
+ manifestCount: manifestDirs.length,
1104
+ serviceCount: services.length,
1105
+ services,
1106
+ warnings
1107
+ };
1108
+ }
1109
+ function discoverRemoteHostManifests(projectRoot) {
1110
+ const workspaceRoot = resolveRuntimeWorkspaceRoot(projectRoot);
1111
+ return [
1112
+ join(projectRoot, "rig.remote-hosts.yaml"),
1113
+ resolve2(workspaceRoot, ".rig", "remote-hosts.yaml")
1114
+ ].filter((candidate) => existsSync2(candidate));
1115
+ }
1116
+ function readWorkspaceRemoteFleet(projectRoot, updatedAt = new Date().toISOString()) {
1117
+ const manifestPaths = discoverRemoteHostManifests(projectRoot);
1118
+ const warnings = [];
1119
+ const hostsById = new Map;
1120
+ for (const manifestPath of manifestPaths) {
1121
+ try {
1122
+ const parsed = asRecord(parseYamlSubset(readFileSync2(manifestPath, "utf-8")));
1123
+ const hosts2 = Array.isArray(parsed?.hosts) ? parsed.hosts : [];
1124
+ for (const entry of hosts2) {
1125
+ const host = asRecord(entry);
1126
+ const id = asString(host?.id);
1127
+ const name = asString(host?.name);
1128
+ const baseUrl = asString(host?.base_url);
1129
+ if (!id || !name || !baseUrl) {
1130
+ warnings.push(`Skipped invalid remote host entry in ${relativePath(projectRoot, manifestPath)}`);
1131
+ continue;
1132
+ }
1133
+ hostsById.set(id, {
1134
+ id,
1135
+ name,
1136
+ baseUrl,
1137
+ workspacePath: asString(host?.workspace_path),
1138
+ transport: asString(host?.transport) ?? "websocket",
1139
+ hostname: asString(host?.hostname),
1140
+ region: asString(host?.region),
1141
+ labels: asStringArray(host?.labels),
1142
+ capabilities: asStringArray(host?.capabilities),
1143
+ runtimeAdapters: ["pi"],
1144
+ status: asString(host?.status) ?? "offline",
1145
+ currentLeaseCount: typeof host?.current_lease_count === "number" ? Number(host.current_lease_count) : 0,
1146
+ manifestPath: relativePath(projectRoot, manifestPath)
1147
+ });
1148
+ }
1149
+ } catch (error) {
1150
+ warnings.push(`Failed to parse ${relativePath(projectRoot, manifestPath)}: ${error instanceof Error ? error.message : String(error)}`);
1151
+ }
1152
+ }
1153
+ const hosts = [...hostsById.values()].sort((left, right) => left.name.localeCompare(right.name));
1154
+ const onlineHostCount = hosts.filter((host) => ONLINE_REMOTE_HOST_STATUSES.has(host.status)).length;
1155
+ return {
1156
+ updatedAt,
1157
+ status: hosts.length === 0 ? "empty" : warnings.length > 0 || hosts.some((host) => host.status === "degraded" || host.status === "quarantined") ? "degraded" : onlineHostCount > 0 ? "ready" : "degraded",
1158
+ manifestCount: manifestPaths.length,
1159
+ hostCount: hosts.length,
1160
+ onlineHostCount,
1161
+ hosts,
1162
+ warnings
1163
+ };
1164
+ }
1165
+ async function readWorkspaceServiceFabric(projectRoot) {
1166
+ try {
1167
+ const updatedAt = new Date().toISOString();
1168
+ const persisted = readPersistedFabricState(projectRoot);
1169
+ const plans = readNativeServicePlans(projectRoot);
1170
+ const services = materializeStateFromPlans(projectRoot, plans.services, persisted);
1171
+ for (const service of services) {
1172
+ if (service.status === "stopped") {
1173
+ continue;
1174
+ }
1175
+ if (!isProcessRunning(service.pid)) {
1176
+ service.pid = null;
1177
+ service.status = "stopped";
1178
+ continue;
1179
+ }
1180
+ service.status = await probeReadiness(service, 1000) ? "healthy" : "degraded";
1181
+ }
1182
+ const state = {
1183
+ updatedAt,
1184
+ rootPath: projectRoot,
1185
+ services
1186
+ };
1187
+ writePersistedFabricState(projectRoot, state);
1188
+ return summarizeFabricState(projectRoot, services, plans.warnings, updatedAt);
1189
+ } catch (error) {
1190
+ return {
1191
+ status: "unavailable",
1192
+ warnings: [error instanceof Error ? error.message : String(error)]
1193
+ };
1194
+ }
1195
+ }
1196
+ async function mutateWorkspaceServiceFabric(projectRoot, command, services = []) {
1197
+ const updatedAt = new Date().toISOString();
1198
+ const persisted = readPersistedFabricState(projectRoot);
1199
+ const plans = readNativeServicePlans(projectRoot, services);
1200
+ const stateServices = materializeStateFromPlans(projectRoot, plans.services, persisted);
1201
+ if (command === "verify") {
1202
+ for (const service of stateServices) {
1203
+ if (service.status === "stopped") {
1204
+ continue;
1205
+ }
1206
+ if (!isProcessRunning(service.pid)) {
1207
+ service.pid = null;
1208
+ service.status = "stopped";
1209
+ continue;
1210
+ }
1211
+ service.status = await probeReadiness(service, 1000) ? "healthy" : "degraded";
1212
+ }
1213
+ } else if (command === "up") {
1214
+ const { logsDir } = resolveServiceFabricPaths(projectRoot);
1215
+ mkdirSync(logsDir, { recursive: true });
1216
+ for (const service of stateServices) {
1217
+ if (service.mode === "stub") {
1218
+ service.pid = null;
1219
+ service.status = "healthy";
1220
+ continue;
1221
+ }
1222
+ if (isProcessRunning(service.pid) && await probeReadiness(service, 1000)) {
1223
+ service.status = "healthy";
1224
+ continue;
1225
+ }
1226
+ if (isProcessRunning(service.pid)) {
1227
+ await stopProcess(service.pid);
1228
+ }
1229
+ const logPath = resolve2(logsDir, `${service.name}.log`);
1230
+ service.logPath = logPath;
1231
+ appendFileSync(logPath, `
1232
+ === ${updatedAt} :: rig service-fabric up :: ${service.name} ===
1233
+ `, "utf-8");
1234
+ const logFd = openSync(logPath, "a");
1235
+ try {
1236
+ const child = spawn(service.command || "true", {
1237
+ cwd: service.workingDir,
1238
+ env: { ...process.env, ...service.environment },
1239
+ shell: true,
1240
+ detached: true,
1241
+ stdio: ["ignore", logFd, logFd]
1242
+ });
1243
+ child.unref();
1244
+ service.pid = child.pid ?? null;
1245
+ service.status = "booting";
1246
+ } finally {
1247
+ closeSync(logFd);
1248
+ }
1249
+ service.status = await waitForServiceHealthy(service) ? "healthy" : "degraded";
1250
+ if (service.status !== "healthy" && !isProcessRunning(service.pid)) {
1251
+ service.pid = null;
1252
+ service.status = "failed";
1253
+ }
1254
+ }
1255
+ } else if (command === "down") {
1256
+ for (const service of stateServices) {
1257
+ await stopProcess(service.pid);
1258
+ service.pid = null;
1259
+ service.status = "stopped";
1260
+ }
1261
+ }
1262
+ const merged = new Map;
1263
+ for (const service of persisted?.services ?? []) {
1264
+ merged.set(service.name, service);
1265
+ }
1266
+ for (const service of stateServices) {
1267
+ merged.set(service.name, service);
1268
+ }
1269
+ const nextState = {
1270
+ updatedAt,
1271
+ rootPath: projectRoot,
1272
+ services: [...merged.values()].sort((left, right) => left.name.localeCompare(right.name))
1273
+ };
1274
+ writePersistedFabricState(projectRoot, nextState);
1275
+ return summarizeFabricState(projectRoot, stateServices, plans.warnings, updatedAt);
1276
+ }
1277
+ function countRuntimeDirs(projectRoot) {
1278
+ const runtimeRoot = resolve2(resolveRuntimeWorkspaceRoot(projectRoot), ".rig", "runtime", "agents");
1279
+ if (!existsSync2(runtimeRoot)) {
1280
+ return 0;
1281
+ }
1282
+ return readdirSync(runtimeRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
1283
+ }
1284
+ function countArtifactTaskDirs(projectRoot) {
1285
+ const artifactRoot = resolve2(resolveRuntimeWorkspaceRoot(projectRoot), "artifacts");
1286
+ if (!existsSync2(artifactRoot)) {
1287
+ return 0;
1288
+ }
1289
+ return readdirSync(artifactRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name !== "remote-runs").length;
1290
+ }
1291
+ async function readWorkspaceSummary(projectRoot) {
1292
+ const generatedAt = new Date().toISOString();
1293
+ const warnings = [];
1294
+ const topology = readWorkspaceTopology(projectRoot, generatedAt);
1295
+ const remoteFleet = readWorkspaceRemoteFleet(projectRoot, generatedAt);
1296
+ const serviceFabric = await readWorkspaceServiceFabric(projectRoot);
1297
+ if (serviceFabric?.warnings) {
1298
+ warnings.push(...serviceFabric.warnings.map((warning) => String(warning)));
1299
+ }
1300
+ warnings.push(...topology.warnings, ...remoteFleet.warnings);
1301
+ return {
1302
+ rootPath: projectRoot,
1303
+ taskCounts: collectTaskCounts(projectRoot),
1304
+ runtimeCount: countRuntimeDirs(projectRoot),
1305
+ artifactTaskCount: countArtifactTaskDirs(projectRoot),
1306
+ failedApproachesPresent: existsSync2(resolve2(resolveRuntimeWorkspaceRoot(projectRoot), ".rig", "state", "failed_approaches.md")),
1307
+ topology,
1308
+ remoteFleet,
1309
+ serviceFabric,
1310
+ warnings,
1311
+ generatedAt
1312
+ };
1313
+ }
1314
+
1315
+ // packages/cli-surface-plugin/src/commands/workspace.ts
51
1316
  async function executeWorkspace(context, args) {
52
1317
  const [command = "summary", ...rest] = args;
53
1318
  switch (command) {