@h-rig/runtime 0.0.6-alpha.32 → 0.0.6-alpha.34

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.
@@ -25,1631 +25,1639 @@ function readBuildConfig() {
25
25
  }
26
26
  }
27
27
 
28
- // packages/runtime/src/control-plane/runtime/context.ts
29
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
30
- import { dirname, resolve } from "path";
31
- var RUNTIME_CONTEXT_ENV = "RIG_RUNTIME_CONTEXT_FILE";
32
- var runtimeContextStringFields = [
33
- "runtimeId",
34
- "taskId",
35
- "role",
36
- "workspaceDir",
37
- "stateDir",
38
- "logsDir",
39
- "sessionDir",
40
- "sessionFile",
41
- "policyFile",
42
- "binDir",
43
- "createdAt"
44
- ];
45
- var runtimeContextArrayFields = ["scopes", "validation"];
46
- var runtimeContextOptionalStringFields = [
47
- "artifactRoot",
48
- "hostProjectRoot",
49
- "monorepoMainRoot",
50
- "monorepoBaseRef",
51
- "monorepoBaseCommit"
52
- ];
53
- function loadRuntimeContext(path) {
54
- const absPath = resolve(path);
55
- if (!existsSync(absPath)) {
56
- throw new Error(`RuntimeTaskContext file not found: ${absPath}`);
57
- }
58
- let raw;
28
+ // packages/runtime/src/control-plane/native/git-native.ts
29
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
30
+ import { tmpdir } from "os";
31
+ import { dirname, isAbsolute, resolve } from "path";
32
+ import { createHash } from "crypto";
33
+ function isTextTreeCommitUpdate(update) {
34
+ return typeof update.content === "string";
35
+ }
36
+ var sharedGitNativeOutputDir = resolve(tmpdir(), "rig-native");
37
+ var sharedGitNativeOutputPath = resolve(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
38
+ var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
39
+ function temporaryGitBinaryOutputPath(outputPath) {
40
+ const suffix = process.platform === "win32" ? ".exe" : "";
41
+ return resolve(dirname(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
42
+ }
43
+ function publishGitBinary(tempOutputPath, outputPath) {
59
44
  try {
60
- raw = JSON.parse(readFileSync(absPath, "utf8"));
61
- } catch (err) {
62
- throw new Error(`Failed to parse RuntimeTaskContext at ${absPath}: ${String(err)}`);
63
- }
64
- if (typeof raw !== "object" || raw === null) {
65
- throw new Error(`RuntimeTaskContext at ${absPath} is not an object`);
66
- }
67
- const obj = raw;
68
- for (const field of runtimeContextStringFields) {
69
- if (typeof obj[field] !== "string" || obj[field].length === 0) {
70
- throw new Error(`RuntimeTaskContext field "${field}" must be a non-empty string (at ${absPath})`);
71
- }
72
- }
73
- for (const field of runtimeContextArrayFields) {
74
- if (!Array.isArray(obj[field])) {
75
- throw new Error(`RuntimeTaskContext field "${field}" must be an array (at ${absPath})`);
76
- }
77
- if (!obj[field].every((entry) => typeof entry === "string")) {
78
- throw new Error(`RuntimeTaskContext field "${field}" must be a string[] (at ${absPath})`);
45
+ renameSync(tempOutputPath, outputPath);
46
+ } catch (error) {
47
+ if (process.platform === "win32" && existsSync(outputPath)) {
48
+ rmSync(outputPath, { force: true });
49
+ renameSync(tempOutputPath, outputPath);
50
+ return;
79
51
  }
52
+ throw error;
80
53
  }
81
- for (const field of runtimeContextOptionalStringFields) {
82
- if (field in obj && obj[field] !== undefined && typeof obj[field] !== "string") {
83
- throw new Error(`RuntimeTaskContext field "${field}" must be a string when present (at ${absPath})`);
84
- }
54
+ }
55
+ function runtimeRigGitFileName() {
56
+ return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
57
+ }
58
+ function rigGitSourceCandidates() {
59
+ const execDir = process.execPath?.trim() ? dirname(process.execPath.trim()) : "";
60
+ const cwd = process.cwd()?.trim() || "";
61
+ const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
62
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
63
+ const moduleRelativeSource = resolve(import.meta.dir, "../../../native/rig-git.zig");
64
+ return [...new Set([
65
+ process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
66
+ moduleRelativeSource,
67
+ projectRoot ? resolve(projectRoot, "packages/runtime/native/rig-git.zig") : "",
68
+ hostProjectRoot ? resolve(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
69
+ cwd ? resolve(cwd, "packages/runtime/native/rig-git.zig") : "",
70
+ execDir ? resolve(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
71
+ execDir ? resolve(execDir, "..", "native", "rig-git.zig") : ""
72
+ ].filter(Boolean))];
73
+ }
74
+ function nativePackageBinaryCandidates(fromDir, fileName) {
75
+ const candidates = [];
76
+ let cursor = resolve(fromDir);
77
+ for (let index = 0;index < 8; index += 1) {
78
+ candidates.push(resolve(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve(cursor, "native", fileName), resolve(cursor, "native", "bin", fileName));
79
+ const parent = dirname(cursor);
80
+ if (parent === cursor)
81
+ break;
82
+ cursor = parent;
85
83
  }
86
- if (obj.browser !== undefined) {
87
- if (typeof obj.browser !== "object" || obj.browser === null || Array.isArray(obj.browser)) {
88
- throw new Error(`RuntimeTaskContext field "browser" must be an object when present (at ${absPath})`);
89
- }
90
- const browser = obj.browser;
91
- for (const field of [
92
- "preset",
93
- "mode",
94
- "stateDir",
95
- "defaultProfile",
96
- "effectiveProfile",
97
- "defaultAttachUrl",
98
- "effectiveAttachUrl",
99
- "launchHelper",
100
- "checkHelper",
101
- "attachInfoHelper",
102
- "e2eHelper",
103
- "resetProfileHelper"
104
- ]) {
105
- if (typeof browser[field] !== "string" || browser[field].length === 0) {
106
- throw new Error(`RuntimeTaskContext field "browser.${field}" must be a non-empty string (at ${absPath})`);
107
- }
108
- }
109
- for (const field of ["devCommand", "launchCommand", "checkCommand", "e2eCommand"]) {
110
- if (browser[field] !== undefined && typeof browser[field] !== "string") {
111
- throw new Error(`RuntimeTaskContext field "browser.${field}" must be a string when present (at ${absPath})`);
112
- }
113
- }
114
- if (typeof browser.required !== "boolean") {
115
- throw new Error(`RuntimeTaskContext field "browser.required" must be a boolean (at ${absPath})`);
84
+ return candidates;
85
+ }
86
+ function rigGitBinaryCandidates() {
87
+ const execDir = process.execPath?.trim() ? dirname(process.execPath.trim()) : "";
88
+ const fileName = runtimeRigGitFileName();
89
+ const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
90
+ return [...new Set([
91
+ explicit,
92
+ ...nativePackageBinaryCandidates(import.meta.dir, fileName),
93
+ execDir ? resolve(execDir, fileName) : "",
94
+ execDir ? resolve(execDir, "..", fileName) : "",
95
+ execDir ? resolve(execDir, "..", "bin", fileName) : "",
96
+ sharedGitNativeOutputPath
97
+ ].filter(Boolean))];
98
+ }
99
+ function resolveGitSourcePath() {
100
+ for (const candidate of rigGitSourceCandidates()) {
101
+ if (candidate && existsSync(candidate)) {
102
+ return candidate;
116
103
  }
117
104
  }
118
- if (obj.memory !== undefined) {
119
- if (typeof obj.memory !== "object" || obj.memory === null || Array.isArray(obj.memory)) {
120
- throw new Error(`RuntimeTaskContext field "memory" must be an object when present (at ${absPath})`);
121
- }
122
- const memory = obj.memory;
123
- for (const field of ["canonicalPath", "canonicalRef", "canonicalBaseOid", "hydratedPath"]) {
124
- if (typeof memory[field] !== "string" || memory[field].length === 0) {
125
- throw new Error(`RuntimeTaskContext field "memory.${field}" must be a non-empty string (at ${absPath})`);
126
- }
127
- }
128
- if (typeof memory.createdFresh !== "boolean") {
129
- throw new Error(`RuntimeTaskContext field "memory.createdFresh" must be a boolean (at ${absPath})`);
130
- }
131
- if (typeof memory.retrieval !== "object" || memory.retrieval === null || Array.isArray(memory.retrieval)) {
132
- throw new Error(`RuntimeTaskContext field "memory.retrieval" must be an object (at ${absPath})`);
133
- }
134
- const retrieval = memory.retrieval;
135
- for (const field of ["topK", "lexicalWeight", "vectorWeight", "recencyWeight", "confidenceWeight"]) {
136
- if (typeof retrieval[field] !== "number" || Number.isNaN(retrieval[field])) {
137
- throw new Error(`RuntimeTaskContext field "memory.retrieval.${field}" must be a number (at ${absPath})`);
138
- }
139
- }
105
+ return null;
106
+ }
107
+ function resolveGitBinaryPath() {
108
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
109
+ return null;
140
110
  }
141
- if (obj.initialDirtyFiles !== undefined) {
142
- if (typeof obj.initialDirtyFiles !== "object" || obj.initialDirtyFiles === null || Array.isArray(obj.initialDirtyFiles)) {
143
- throw new Error(`RuntimeTaskContext field "initialDirtyFiles" must be an object when present (at ${absPath})`);
144
- }
145
- const dirtyFiles = obj.initialDirtyFiles;
146
- for (const key of ["project", "monorepo"]) {
147
- if (dirtyFiles[key] === undefined) {
148
- continue;
149
- }
150
- if (!Array.isArray(dirtyFiles[key]) || !dirtyFiles[key].every((entry) => typeof entry === "string")) {
151
- throw new Error(`RuntimeTaskContext field "initialDirtyFiles.${key}" must be a string[] when present (at ${absPath})`);
152
- }
111
+ for (const candidate of rigGitBinaryCandidates()) {
112
+ if (candidate && existsSync(candidate)) {
113
+ return candidate;
153
114
  }
154
115
  }
155
- if (obj.initialHeadCommits !== undefined) {
156
- if (typeof obj.initialHeadCommits !== "object" || obj.initialHeadCommits === null || Array.isArray(obj.initialHeadCommits)) {
157
- throw new Error(`RuntimeTaskContext field "initialHeadCommits" must be an object when present (at ${absPath})`);
158
- }
159
- const headCommits = obj.initialHeadCommits;
160
- for (const key of ["project", "monorepo"]) {
161
- if (headCommits[key] === undefined) {
162
- continue;
163
- }
164
- if (typeof headCommits[key] !== "string") {
165
- throw new Error(`RuntimeTaskContext field "initialHeadCommits.${key}" must be a string when present (at ${absPath})`);
166
- }
116
+ return null;
117
+ }
118
+ function preferredGitBinaryOutputPath() {
119
+ const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
120
+ return explicit || sharedGitNativeOutputPath;
121
+ }
122
+ function binarySupportsTrackerCommandsSync(binaryPath) {
123
+ try {
124
+ const probe = Bun.spawnSync([binaryPath, "fetch-ref", "."], {
125
+ stdout: "pipe",
126
+ stderr: "pipe"
127
+ });
128
+ const stdout = probe.stdout.toString().trim();
129
+ const stderr = probe.stderr.toString().trim();
130
+ if (stdout.includes('"error":"unknown command"')) {
131
+ return false;
167
132
  }
133
+ return probe.exitCode === 2 && stderr.includes(trackerCommandUsageProbe);
134
+ } catch {
135
+ return false;
168
136
  }
169
- return obj;
170
137
  }
171
- function loadRuntimeContextFromEnv(env = process.env) {
172
- const contextFile = env[RUNTIME_CONTEXT_ENV];
173
- if (contextFile) {
174
- return loadRuntimeContext(contextFile);
138
+ function nativeBuildManifestPath(outputPath) {
139
+ return `${outputPath}.build-manifest.json`;
140
+ }
141
+ function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
142
+ if (!existsSync(manifestPath)) {
143
+ return false;
175
144
  }
176
- const inferred = findRuntimeContextFile(process.cwd());
177
- if (!inferred) {
178
- return null;
145
+ try {
146
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
147
+ return manifest.version === 1 && manifest.buildKey === buildKey;
148
+ } catch {
149
+ return false;
179
150
  }
180
- return loadRuntimeContext(inferred);
181
151
  }
182
- function findRuntimeContextFile(startPath) {
183
- let current = resolve(startPath);
184
- while (true) {
185
- const candidate = resolve(current, "runtime-context.json");
186
- if (existsSync(candidate) && isAgentRuntimeContextPath(candidate)) {
187
- return candidate;
188
- }
189
- const parent = dirname(current);
190
- if (parent === current) {
191
- return "";
152
+ function sha256FileSync(path) {
153
+ return createHash("sha256").update(readFileSync(path)).digest("hex");
154
+ }
155
+ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
156
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
157
+ throw new Error("Zig native git is disabled via RIG_DISABLE_ZIG_NATIVE=1");
158
+ }
159
+ const sourcePath = resolveGitSourcePath();
160
+ if (!sourcePath) {
161
+ const binaryPath = resolveGitBinaryPath();
162
+ if (binaryPath) {
163
+ return binaryPath;
192
164
  }
193
- current = parent;
165
+ throw new Error("rig-git.zig source file not found.");
194
166
  }
195
- }
196
- function isAgentRuntimeContextPath(path) {
197
- const normalized = path.replace(/\\/g, "/");
198
- return /\/\.rig\/runtime-context\.json$/.test(normalized);
199
- }
200
-
201
- // packages/runtime/src/control-plane/runtime/tooling/shell.ts
202
- import { tmpdir } from "os";
203
- import { basename, dirname as dirname2, resolve as resolve2 } from "path";
204
- var sharedNativeShellOutputDir = resolve2(tmpdir(), "rig-native");
205
- var sharedNativeShellOutputPath = resolve2(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
206
- function runtimeToolGatewayNames() {
207
- return [
208
- "bash",
209
- "sh",
210
- "zsh",
211
- "git",
212
- "bun",
213
- "node",
214
- "python3",
215
- "rg",
216
- "grep",
217
- "sed",
218
- "cat",
219
- "ls",
220
- "find",
221
- "tsc",
222
- "gh",
223
- "mkdir",
224
- "rm",
225
- "mv",
226
- "cp",
227
- "touch",
228
- "pwd",
229
- "head",
230
- "tail",
231
- "wc",
232
- "sort",
233
- "uniq",
234
- "awk",
235
- "xargs",
236
- "dirname",
237
- "basename",
238
- "realpath",
239
- "env",
240
- "jq",
241
- "tee",
242
- "which"
243
- ];
244
- }
245
- // packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
246
- import { tmpdir as tmpdir2 } from "os";
247
- import { basename as basename2, dirname as dirname3, resolve as resolve3 } from "path";
248
- var sharedNativeToolsOutputDir = resolve3(tmpdir2(), "rig-native");
249
- var sharedNativeToolsOutputPath = resolve3(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
250
- function runtimeFileToolNames() {
251
- return [
252
- "rig-read",
253
- "rig-write",
254
- "rig-edit",
255
- "rig-glob",
256
- "rig-grep"
257
- ];
258
- }
259
-
260
- // packages/runtime/src/control-plane/runtime/tooling/gateway.ts
261
- function runtimeGatewayToolNames() {
262
- return runtimeToolGatewayNames();
263
- }
264
- // packages/runtime/src/control-plane/browser-contract.ts
265
- import { resolve as resolve4 } from "path";
266
- var DEFAULT_BROWSER_ATTACH_URL = "http://127.0.0.1:9333";
267
- var DEFAULT_BROWSER_MODE = "persistent";
268
- var RUNTIME_BROWSER_HELPERS = {
269
- launch: "rig-browser-launch",
270
- check: "rig-browser-check",
271
- attachInfo: "rig-browser-attach-info",
272
- e2e: "rig-browser-e2e",
273
- resetProfile: "rig-browser-reset-profile"
274
- };
275
- var BASE_REMOTE_DEBUGGING_PORT = 9222;
276
- var REMOTE_DEBUGGING_PORT_SPREAD = 4000;
277
- function hashString(input) {
278
- let hash = 0;
279
- for (let index = 0;index < input.length; index += 1) {
280
- hash = (hash << 5) - hash + input.charCodeAt(index) | 0;
281
- }
282
- return Math.abs(hash);
283
- }
284
- function derivePortFromProfile(profileName) {
285
- return BASE_REMOTE_DEBUGGING_PORT + hashString(profileName) % REMOTE_DEBUGGING_PORT_SPREAD;
286
- }
287
- function parseAttachUrl(attachUrl) {
288
- try {
289
- return new URL((attachUrl || DEFAULT_BROWSER_ATTACH_URL).trim() || DEFAULT_BROWSER_ATTACH_URL);
290
- } catch {
291
- return new URL(DEFAULT_BROWSER_ATTACH_URL);
292
- }
293
- }
294
- function sanitizeRuntimeSuffix(runtimeId) {
295
- return runtimeId.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 16) || "runtime";
296
- }
297
- function resolveBrowserStateDir(projectRoot, configuredStateDir) {
298
- const trimmed = configuredStateDir?.trim() || ".tmp/rig-browser";
299
- if (trimmed.startsWith("/")) {
300
- return resolve4(trimmed);
167
+ const zigBinary = Bun.which("zig");
168
+ if (!zigBinary) {
169
+ throw new Error("zig is required to build native Rig git tools.");
301
170
  }
302
- return resolve4(projectRoot || process.cwd(), trimmed);
303
- }
304
- function resolveTaskBrowserContext(browser, options = {}) {
305
- if (!browser?.required) {
306
- return;
171
+ mkdirSync(dirname(outputPath), { recursive: true });
172
+ const sourceDigest = sha256FileSync(sourcePath);
173
+ const buildKey = JSON.stringify({
174
+ version: 1,
175
+ zigBinary,
176
+ platform: process.platform,
177
+ arch: process.arch,
178
+ sourcePath,
179
+ sourceDigest
180
+ });
181
+ const manifestPath = nativeBuildManifestPath(outputPath);
182
+ const needsBuild = !existsSync(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
183
+ if (!needsBuild) {
184
+ chmodSync(outputPath, 493);
185
+ return outputPath;
307
186
  }
308
- const defaultProfile = browser.profile?.trim() || "default";
309
- const mode = browser.mode?.trim() || DEFAULT_BROWSER_MODE;
310
- const defaultAttach = parseAttachUrl(browser.attach_url);
311
- const shouldDeriveRuntimeProfile = Boolean(options.runtimeId?.trim()) && mode !== "shared";
312
- const effectiveProfile = shouldDeriveRuntimeProfile ? `${defaultProfile}-${sanitizeRuntimeSuffix(options.runtimeId.trim())}` : defaultProfile;
313
- const effectivePort = shouldDeriveRuntimeProfile ? derivePortFromProfile(effectiveProfile) : Number(defaultAttach.port || "80");
314
- const effectiveAttach = new URL(defaultAttach.toString());
315
- effectiveAttach.port = String(effectivePort);
316
- return {
317
- required: true,
318
- preset: browser.preset?.trim() || "default",
319
- mode,
320
- stateDir: resolveBrowserStateDir(options.hostProjectRoot, browser.state_dir),
321
- defaultProfile,
322
- effectiveProfile,
323
- defaultAttachUrl: defaultAttach.toString(),
324
- effectiveAttachUrl: effectiveAttach.toString(),
325
- devCommand: browser.dev_command?.trim() || undefined,
326
- launchCommand: browser.launch_command?.trim() || undefined,
327
- checkCommand: browser.check_command?.trim() || undefined,
328
- e2eCommand: browser.e2e_command?.trim() || undefined,
329
- launchHelper: RUNTIME_BROWSER_HELPERS.launch,
330
- checkHelper: RUNTIME_BROWSER_HELPERS.check,
331
- attachInfoHelper: RUNTIME_BROWSER_HELPERS.attachInfo,
332
- e2eHelper: RUNTIME_BROWSER_HELPERS.e2e,
333
- resetProfileHelper: RUNTIME_BROWSER_HELPERS.resetProfile
334
- };
335
- }
336
- function buildBrowserGuidanceLines(browser) {
337
- const lines = [
338
- "This task requires Rig Browser.",
339
- `Launch the browser: \`${browser.launchHelper}\`${browser.devCommand ? " or `rig-browser-launch --dev`" : ""}.`,
340
- `Check the browser contract: \`${browser.checkHelper}\`.`,
341
- `Show attach details: \`${browser.attachInfoHelper}\`.`,
342
- `Attach Chrome DevTools MCP to ${browser.effectiveAttachUrl}.`,
343
- `Preset: ${browser.preset}.`,
344
- `Profile: ${browser.effectiveProfile}.`,
345
- `State dir: ${browser.stateDir}.`,
346
- `Reset the active profile with \`${browser.resetProfileHelper}\`.`
347
- ];
348
- if (browser.e2eCommand) {
349
- lines.push(`Run app-owned browser e2e with \`${browser.e2eHelper}\`.`);
187
+ const tempOutputPath = temporaryGitBinaryOutputPath(outputPath);
188
+ const build = Bun.spawnSync([
189
+ zigBinary,
190
+ "build-exe",
191
+ sourcePath,
192
+ "-O",
193
+ "ReleaseFast",
194
+ `-femit-bin=${tempOutputPath}`
195
+ ], {
196
+ cwd: dirname(sourcePath),
197
+ stdout: "pipe",
198
+ stderr: "pipe"
199
+ });
200
+ if (build.exitCode !== 0 || !existsSync(tempOutputPath)) {
201
+ const stderr = build.stderr.toString().trim();
202
+ const stdout = build.stdout.toString().trim();
203
+ const details = [stderr, stdout].filter(Boolean).join(`
204
+ `);
205
+ throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
350
206
  }
351
- if (browser.defaultProfile !== browser.effectiveProfile) {
352
- lines.push(`Base profile: ${browser.defaultProfile}. Runtime launches derive an isolated effective profile.`);
207
+ chmodSync(tempOutputPath, 493);
208
+ if (existsSync(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
209
+ rmSync(tempOutputPath, { force: true });
210
+ chmodSync(outputPath, 493);
211
+ return outputPath;
353
212
  }
354
- return lines;
355
- }
356
-
357
- // packages/runtime/src/control-plane/plugin-host-context.ts
358
- import { createPluginHost } from "@rig/core";
359
- import { loadConfig } from "@rig/core/load-config";
360
-
361
- // packages/runtime/src/control-plane/task-source.ts
362
- function createTaskSourceRegistry() {
363
- const byId = new Map;
364
- const order = [];
365
- return {
366
- register(s) {
367
- if (byId.has(s.id))
368
- throw new Error(`task source already registered: ${s.id}`);
369
- byId.set(s.id, s);
370
- order.push(s);
371
- },
372
- resolveById(id) {
373
- const s = byId.get(id);
374
- if (!s)
375
- throw new Error(`task source not registered: ${id}`);
376
- return s;
377
- },
378
- resolveByKind(kind) {
379
- for (const s of order)
380
- if (s.kind === kind)
381
- return s;
382
- throw new Error(`no task source registered for kind: ${kind}`);
383
- },
384
- list: () => order
385
- };
386
- }
387
-
388
- // packages/runtime/src/control-plane/task-source-bootstrap.ts
389
- function formatRegisteredKinds(pluginHost) {
390
- const kinds = pluginHost ? pluginHost.listExecutableTaskSources().map((source) => source.kind) : [];
391
- return kinds.length > 0 ? kinds.join(", ") : "none";
392
- }
393
- function buildTaskSourceRegistry(config, pluginHost) {
394
- const registry = createTaskSourceRegistry();
395
- const taskSourceConfig = config.taskSource;
396
- const factory = pluginHost?.resolveTaskSourceFactoryByKind(taskSourceConfig.kind);
397
- if (!factory) {
398
- throw new Error(`No task source factory registered for kind "${taskSourceConfig.kind}". ` + `Registered kinds: ${formatRegisteredKinds(pluginHost)}. ` + "Load a plugin that contributes an executable task source factory for this kind.");
213
+ publishGitBinary(tempOutputPath, outputPath);
214
+ if (!binarySupportsTrackerCommandsSync(outputPath)) {
215
+ rmSync(outputPath, { force: true });
216
+ throw new Error("Failed to build native Rig git tools: tracker command probe failed");
399
217
  }
400
- registry.register(factory.factory(taskSourceConfig));
401
- return registry;
218
+ writeFileSync(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
219
+ `, "utf8");
220
+ return outputPath;
402
221
  }
403
-
404
- // packages/runtime/src/control-plane/repos/registry.ts
405
- function createRepoRegistry(entries) {
406
- const map = new Map;
407
- for (const e of entries) {
408
- if (map.has(e.id))
409
- throw new Error(`repo already registered: ${e.id}`);
410
- map.set(e.id, { ...e });
222
+ function runGitNative(command, args) {
223
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
224
+ return { ok: false, error: "rig-git native disabled" };
411
225
  }
412
- const ordered = Array.from(map.values());
413
- return {
414
- getById: (id) => map.get(id),
415
- list: () => ordered
416
- };
417
- }
418
- var MANAGED_REPOS = new Map;
419
- function setManagedRepos(entries) {
420
- const next = new Map;
421
- for (const e of entries) {
422
- if (next.has(e.id)) {
423
- throw new Error(`managed repo already registered: ${e.id}`);
226
+ const trackerCommand = command === "fetch-ref" || command === "read-blob-at-ref" || command === "write-tree-commit" || command === "push-ref-with-lease";
227
+ let binaryPath = null;
228
+ if (trackerCommand) {
229
+ try {
230
+ binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
231
+ } catch (error) {
232
+ const message = error instanceof Error ? error.message : String(error);
233
+ if (message.includes("rig-git.zig source file not found")) {
234
+ return { ok: false, error: "rig-git binary not found" };
235
+ }
236
+ return { ok: false, error: message };
424
237
  }
425
- next.set(e.id, e);
426
- }
427
- MANAGED_REPOS = next;
428
- }
429
- function getManagedRepoEntry(repoId) {
430
- const entry = MANAGED_REPOS.get(repoId);
431
- if (!entry) {
432
- throw new Error(`managed repo not registered: ${repoId}. Plugins contribute repos via RigPlugin.contributes.repoSources; ` + `make sure a plugin declares this id and the plugin host has been initialized.`);
433
- }
434
- return entry;
435
- }
436
- function listManagedRepoEntries() {
437
- return Array.from(MANAGED_REPOS.values());
438
- }
439
- function repoRegistrationToManagedEntry(reg) {
440
- if (!reg.defaultBranch) {
441
- return null;
442
- }
443
- return {
444
- id: reg.id,
445
- alias: reg.defaultPath ?? reg.id,
446
- defaultBranch: reg.defaultBranch,
447
- defaultRemoteUrl: reg.url,
448
- remoteEnvVar: reg.remoteEnvVar,
449
- checkoutEnvVar: reg.checkoutEnvVar
450
- };
451
- }
452
-
453
- // packages/runtime/src/control-plane/agent-roles.ts
454
- function createAgentRoleRegistry(pluginRoles, configOverrides) {
455
- const map = new Map;
456
- for (const r of pluginRoles) {
457
- if (map.has(r.id))
458
- throw new Error(`agent role already registered: ${r.id}`);
459
- const override = configOverrides?.[r.id];
460
- const model = override?.model ?? r.defaultModel;
461
- if (!model) {
462
- throw new Error(`agent role "${r.id}" has no model \u2014 provide defaultModel in plugin or model in config.runtime.agentRoles.${r.id}`);
238
+ } else {
239
+ const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
240
+ binaryPath = explicitBinaryPath && existsSync(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
241
+ if (!binaryPath) {
242
+ try {
243
+ binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
244
+ } catch (error) {
245
+ const message = error instanceof Error ? error.message : String(error);
246
+ if (message.includes("rig-git.zig source file not found")) {
247
+ return { ok: false, error: "rig-git binary not found" };
248
+ }
249
+ return { ok: false, error: message };
250
+ }
463
251
  }
464
- map.set(r.id, { ...r, model });
465
- }
466
- return {
467
- resolve(id) {
468
- const r = map.get(id);
469
- if (!r)
470
- throw new Error(`agent role not registered: ${id}`);
471
- return r;
472
- },
473
- list: () => Array.from(map.values())
474
- };
475
- }
476
-
477
- // packages/runtime/src/control-plane/task-fields.ts
478
- function createTaskFieldRegistry(extensions) {
479
- const byId = new Map;
480
- for (const e of extensions) {
481
- if (byId.has(e.id))
482
- throw new Error(`task field extension already registered: ${e.id}`);
483
- byId.set(e.id, e);
484
252
  }
485
- return {
486
- get: (id) => byId.get(id),
487
- list: () => Array.from(byId.values()),
488
- fieldNames: () => Array.from(byId.values()).map((e) => e.fieldName),
489
- validateTaskFields(task) {
490
- const errors = [];
491
- for (const ext of byId.values()) {
492
- let schema;
253
+ try {
254
+ const proc = Bun.spawnSync([binaryPath, command, ...args], {
255
+ stdout: "pipe",
256
+ stderr: "pipe",
257
+ env: process.env
258
+ });
259
+ if (proc.exitCode !== 0) {
260
+ const stdoutText = proc.stdout.toString().trim();
261
+ if (stdoutText) {
493
262
  try {
494
- schema = JSON.parse(ext.schemaJson);
495
- } catch {
496
- errors.push(`task field "${ext.id}": schemaJson is not valid JSON`);
497
- continue;
498
- }
499
- const isRequired = typeof schema === "object" && schema !== null && schema.required === true;
500
- if (!isRequired)
501
- continue;
502
- const value = task[ext.fieldName];
503
- if (value === undefined || value === null || value === "") {
504
- errors.push(`task field "${ext.fieldName}" (from extension "${ext.id}") is required but missing`);
505
- }
263
+ const parsed = JSON.parse(stdoutText);
264
+ if (!parsed.ok) {
265
+ return parsed;
266
+ }
267
+ } catch {}
506
268
  }
507
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
269
+ const errText = proc.stderr.toString().trim() || `exit code ${proc.exitCode}`;
270
+ return { ok: false, error: errText };
508
271
  }
509
- };
272
+ const output = proc.stdout.toString().trim();
273
+ return JSON.parse(output);
274
+ } catch (err) {
275
+ return { ok: false, error: String(err) };
276
+ }
510
277
  }
511
-
512
- // packages/runtime/src/control-plane/validators/runtime-registration.ts
513
- import { existsSync as existsSync2 } from "fs";
514
- import { join } from "path";
515
- function createValidatorRegistry() {
516
- const map = new Map;
517
- const order = [];
518
- const registry = {
519
- register(v) {
520
- if (map.has(v.id))
521
- throw new Error(`validator already registered: ${v.id}`);
522
- map.set(v.id, v);
523
- order.push(v);
524
- },
525
- resolve(id) {
526
- const v = map.get(id);
527
- if (!v)
528
- throw new Error(`validator not registered: ${id}`);
529
- return v;
530
- },
531
- list: () => order
532
- };
533
- registerBuiltInValidators(registry);
534
- return registry;
278
+ function requireGitNative(command, args) {
279
+ const result = runGitNative(command, args);
280
+ if (!result.ok) {
281
+ throw new Error(`rig-git ${command} failed: ${result.error}`);
282
+ }
283
+ return result;
535
284
  }
536
- function registerBuiltInValidators(registry) {
537
- registry.register({
538
- id: "std:typecheck",
539
- category: "custom",
540
- description: "Runs the package typecheck script when present.",
541
- run: async (ctx) => runStdTypecheck(ctx)
542
- });
285
+ function requireGitNativeString(command, args) {
286
+ const result = requireGitNative(command, args);
287
+ if ("value" in result && typeof result.value === "string") {
288
+ return result.value;
289
+ }
290
+ throw new Error(`rig-git ${command} returned an unexpected result payload`);
543
291
  }
544
- async function runStdTypecheck(ctx) {
545
- const packageJsonPath = join(ctx.workspaceRoot, "package.json");
546
- if (!existsSync2(packageJsonPath)) {
547
- return {
548
- id: "std:typecheck",
549
- passed: false,
550
- summary: `package.json not found at ${packageJsonPath}`
551
- };
292
+ function nativePendingFiles(repoPath) {
293
+ const result = runGitNative("pending-files", [repoPath]);
294
+ if (!result.ok)
295
+ return null;
296
+ if ("files" in result && Array.isArray(result.files)) {
297
+ return result.files.map((f) => ({ path: f.path, status: f.status }));
552
298
  }
553
- const proc = Bun.spawn(["bun", "run", "typecheck"], {
554
- cwd: ctx.workspaceRoot,
555
- env: process.env,
556
- stdout: "pipe",
557
- stderr: "pipe"
558
- });
559
- const [exitCode, stdout, stderr] = await Promise.all([
560
- proc.exited,
561
- new Response(proc.stdout).text(),
562
- new Response(proc.stderr).text()
563
- ]);
564
- const output = `${stdout}${stderr}`.trim();
565
- return {
566
- id: "std:typecheck",
567
- passed: exitCode === 0,
568
- summary: exitCode === 0 ? "typecheck passed" : "typecheck failed",
569
- ...output ? { details: output.slice(0, 4000) } : {}
570
- };
299
+ return null;
571
300
  }
572
-
573
- // packages/runtime/src/control-plane/native/scope-rules.ts
574
- var activeRules = null;
575
- function setScopeRules(rules) {
576
- activeRules = rules ?? null;
301
+ function nativeFetchRef(repoPath, remote, branch) {
302
+ return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
577
303
  }
578
- function getScopeRules() {
579
- return activeRules;
304
+ function nativeReadBlobAtRef(repoPath, ref, path) {
305
+ const requestDir = resolve(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
306
+ mkdirSync(requestDir, { recursive: true });
307
+ const outputPath = resolve(requestDir, "blob.txt");
308
+ try {
309
+ requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
310
+ return readFileSync(outputPath, "utf8");
311
+ } finally {
312
+ rmSync(requestDir, { recursive: true, force: true });
313
+ }
580
314
  }
581
-
582
- // packages/runtime/src/control-plane/hook-materializer.ts
583
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
584
- import { dirname as dirname4, resolve as resolve5 } from "path";
585
- var MARKER_PLUGIN = "_rigPlugin";
586
- var MARKER_HOOK_ID = "_rigHookId";
587
- function matcherToString(matcher) {
588
- if (matcher.kind === "all")
589
- return;
590
- if (matcher.kind === "tool")
591
- return matcher.name;
592
- return matcher.pattern;
315
+ function serializeTreeCommitUpdates(updates) {
316
+ return updates.map((update) => {
317
+ if (isTextTreeCommitUpdate(update)) {
318
+ return { path: update.path, kind: "text", content: update.content };
319
+ }
320
+ if (!isAbsolute(update.sourceFilePath)) {
321
+ throw new Error("tree commit binary updates require an absolute sourceFilePath");
322
+ }
323
+ return { path: update.path, kind: "file", sourceFilePath: update.sourceFilePath };
324
+ });
593
325
  }
594
- function isPluginOwned(cmd) {
595
- return typeof cmd[MARKER_PLUGIN] === "string";
326
+ function buildTreeCommitUpdatesJson(updates) {
327
+ return `${JSON.stringify(serializeTreeCommitUpdates(updates), null, 2)}
328
+ `;
596
329
  }
597
- function materializeHooks(projectRoot, entries) {
598
- const settingsPath = resolve5(projectRoot, ".claude", "settings.json");
599
- const existing = existsSync3(settingsPath) ? safeReadJson(settingsPath) : {};
600
- const hooks = existing.hooks ?? {};
601
- for (const event of Object.keys(hooks)) {
602
- const groups = hooks[event] ?? [];
603
- const cleaned = [];
604
- for (const group of groups) {
605
- const operatorHooks = group.hooks.filter((h) => !isPluginOwned(h));
606
- if (operatorHooks.length > 0) {
607
- cleaned.push({ ...group, hooks: operatorHooks });
608
- }
609
- }
610
- if (cleaned.length > 0) {
611
- hooks[event] = cleaned;
612
- } else {
613
- delete hooks[event];
330
+ function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
331
+ const requestDir = resolve(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
332
+ mkdirSync(requestDir, { recursive: true });
333
+ const messagePath = resolve(requestDir, "message.txt");
334
+ const updatesPath = resolve(requestDir, "updates.json");
335
+ try {
336
+ writeFileSync(messagePath, message, "utf8");
337
+ writeFileSync(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
338
+ return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
339
+ } finally {
340
+ rmSync(requestDir, { recursive: true, force: true });
341
+ }
342
+ }
343
+ function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
344
+ return requireGitNativeString("push-ref-with-lease", [
345
+ repoPath,
346
+ localOid,
347
+ remoteRef,
348
+ expectedOldOid,
349
+ remote
350
+ ]);
351
+ }
352
+
353
+ // packages/runtime/src/control-plane/runtime/context.ts
354
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
355
+ import { dirname as dirname2, resolve as resolve2 } from "path";
356
+ var RUNTIME_CONTEXT_ENV = "RIG_RUNTIME_CONTEXT_FILE";
357
+ var runtimeContextStringFields = [
358
+ "runtimeId",
359
+ "taskId",
360
+ "role",
361
+ "workspaceDir",
362
+ "stateDir",
363
+ "logsDir",
364
+ "sessionDir",
365
+ "sessionFile",
366
+ "policyFile",
367
+ "binDir",
368
+ "createdAt"
369
+ ];
370
+ var runtimeContextArrayFields = ["scopes", "validation"];
371
+ var runtimeContextOptionalStringFields = [
372
+ "artifactRoot",
373
+ "hostProjectRoot",
374
+ "monorepoMainRoot",
375
+ "monorepoBaseRef",
376
+ "monorepoBaseCommit"
377
+ ];
378
+ function loadRuntimeContext(path) {
379
+ const absPath = resolve2(path);
380
+ if (!existsSync2(absPath)) {
381
+ throw new Error(`RuntimeTaskContext file not found: ${absPath}`);
382
+ }
383
+ let raw;
384
+ try {
385
+ raw = JSON.parse(readFileSync2(absPath, "utf8"));
386
+ } catch (err) {
387
+ throw new Error(`Failed to parse RuntimeTaskContext at ${absPath}: ${String(err)}`);
388
+ }
389
+ if (typeof raw !== "object" || raw === null) {
390
+ throw new Error(`RuntimeTaskContext at ${absPath} is not an object`);
391
+ }
392
+ const obj = raw;
393
+ for (const field of runtimeContextStringFields) {
394
+ if (typeof obj[field] !== "string" || obj[field].length === 0) {
395
+ throw new Error(`RuntimeTaskContext field "${field}" must be a non-empty string (at ${absPath})`);
614
396
  }
615
397
  }
616
- for (const { pluginName, hook } of entries) {
617
- if (!hook.command) {
618
- continue;
398
+ for (const field of runtimeContextArrayFields) {
399
+ if (!Array.isArray(obj[field])) {
400
+ throw new Error(`RuntimeTaskContext field "${field}" must be an array (at ${absPath})`);
619
401
  }
620
- const event = hook.event;
621
- const matcherString = matcherToString(hook.matcher);
622
- const groups = hooks[event] ??= [];
623
- let group = groups.find((g) => g.matcher === matcherString);
624
- if (!group) {
625
- group = matcherString === undefined ? { hooks: [] } : { matcher: matcherString, hooks: [] };
626
- groups.push(group);
402
+ if (!obj[field].every((entry) => typeof entry === "string")) {
403
+ throw new Error(`RuntimeTaskContext field "${field}" must be a string[] (at ${absPath})`);
627
404
  }
628
- group.hooks.push({
629
- type: "command",
630
- command: hook.command,
631
- [MARKER_PLUGIN]: pluginName,
632
- [MARKER_HOOK_ID]: hook.id
633
- });
634
405
  }
635
- const next = { ...existing };
636
- if (Object.keys(hooks).length > 0) {
637
- next.hooks = hooks;
638
- } else {
639
- delete next.hooks;
406
+ for (const field of runtimeContextOptionalStringFields) {
407
+ if (field in obj && obj[field] !== undefined && typeof obj[field] !== "string") {
408
+ throw new Error(`RuntimeTaskContext field "${field}" must be a string when present (at ${absPath})`);
409
+ }
640
410
  }
641
- mkdirSync2(dirname4(settingsPath), { recursive: true });
642
- writeFileSync2(settingsPath, `${JSON.stringify(next, null, 2)}
643
- `, "utf-8");
644
- return settingsPath;
645
- }
646
- function safeReadJson(path) {
647
- try {
648
- return JSON.parse(readFileSync2(path, "utf-8"));
649
- } catch {
650
- return {};
411
+ if (obj.browser !== undefined) {
412
+ if (typeof obj.browser !== "object" || obj.browser === null || Array.isArray(obj.browser)) {
413
+ throw new Error(`RuntimeTaskContext field "browser" must be an object when present (at ${absPath})`);
414
+ }
415
+ const browser = obj.browser;
416
+ for (const field of [
417
+ "preset",
418
+ "mode",
419
+ "stateDir",
420
+ "defaultProfile",
421
+ "effectiveProfile",
422
+ "defaultAttachUrl",
423
+ "effectiveAttachUrl",
424
+ "launchHelper",
425
+ "checkHelper",
426
+ "attachInfoHelper",
427
+ "e2eHelper",
428
+ "resetProfileHelper"
429
+ ]) {
430
+ if (typeof browser[field] !== "string" || browser[field].length === 0) {
431
+ throw new Error(`RuntimeTaskContext field "browser.${field}" must be a non-empty string (at ${absPath})`);
432
+ }
433
+ }
434
+ for (const field of ["devCommand", "launchCommand", "checkCommand", "e2eCommand"]) {
435
+ if (browser[field] !== undefined && typeof browser[field] !== "string") {
436
+ throw new Error(`RuntimeTaskContext field "browser.${field}" must be a string when present (at ${absPath})`);
437
+ }
438
+ }
439
+ if (typeof browser.required !== "boolean") {
440
+ throw new Error(`RuntimeTaskContext field "browser.required" must be a boolean (at ${absPath})`);
441
+ }
651
442
  }
652
- }
653
-
654
- // packages/runtime/src/control-plane/skill-materializer.ts
655
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
656
- import { resolve as resolve6 } from "path";
657
- import { loadSkill } from "@rig/skill-loader";
658
- var MARKER_FILENAME = ".rig-plugin";
659
- function skillDirName(id) {
660
- return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
661
- }
662
- async function materializeSkills(projectRoot, entries) {
663
- const skillsRoot = resolve6(projectRoot, ".pi", "skills");
664
- if (existsSync4(skillsRoot)) {
665
- for (const name of readdirSync(skillsRoot)) {
666
- const dir = resolve6(skillsRoot, name);
667
- if (existsSync4(resolve6(dir, MARKER_FILENAME))) {
668
- rmSync(dir, { recursive: true, force: true });
443
+ if (obj.memory !== undefined) {
444
+ if (typeof obj.memory !== "object" || obj.memory === null || Array.isArray(obj.memory)) {
445
+ throw new Error(`RuntimeTaskContext field "memory" must be an object when present (at ${absPath})`);
446
+ }
447
+ const memory = obj.memory;
448
+ for (const field of ["canonicalPath", "canonicalRef", "canonicalBaseOid", "hydratedPath"]) {
449
+ if (typeof memory[field] !== "string" || memory[field].length === 0) {
450
+ throw new Error(`RuntimeTaskContext field "memory.${field}" must be a non-empty string (at ${absPath})`);
451
+ }
452
+ }
453
+ if (typeof memory.createdFresh !== "boolean") {
454
+ throw new Error(`RuntimeTaskContext field "memory.createdFresh" must be a boolean (at ${absPath})`);
455
+ }
456
+ if (typeof memory.retrieval !== "object" || memory.retrieval === null || Array.isArray(memory.retrieval)) {
457
+ throw new Error(`RuntimeTaskContext field "memory.retrieval" must be an object (at ${absPath})`);
458
+ }
459
+ const retrieval = memory.retrieval;
460
+ for (const field of ["topK", "lexicalWeight", "vectorWeight", "recencyWeight", "confidenceWeight"]) {
461
+ if (typeof retrieval[field] !== "number" || Number.isNaN(retrieval[field])) {
462
+ throw new Error(`RuntimeTaskContext field "memory.retrieval.${field}" must be a number (at ${absPath})`);
669
463
  }
670
464
  }
671
465
  }
672
- const written = [];
673
- for (const { pluginName, skill } of entries) {
674
- const sourcePath = resolve6(projectRoot, skill.path);
675
- if (!existsSync4(sourcePath)) {
676
- console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
677
- continue;
466
+ if (obj.initialDirtyFiles !== undefined) {
467
+ if (typeof obj.initialDirtyFiles !== "object" || obj.initialDirtyFiles === null || Array.isArray(obj.initialDirtyFiles)) {
468
+ throw new Error(`RuntimeTaskContext field "initialDirtyFiles" must be an object when present (at ${absPath})`);
678
469
  }
679
- let body;
680
- try {
681
- await loadSkill(sourcePath);
682
- body = readFileSync3(sourcePath, "utf-8");
683
- } catch (err) {
684
- console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
685
- continue;
470
+ const dirtyFiles = obj.initialDirtyFiles;
471
+ for (const key of ["project", "monorepo"]) {
472
+ if (dirtyFiles[key] === undefined) {
473
+ continue;
474
+ }
475
+ if (!Array.isArray(dirtyFiles[key]) || !dirtyFiles[key].every((entry) => typeof entry === "string")) {
476
+ throw new Error(`RuntimeTaskContext field "initialDirtyFiles.${key}" must be a string[] when present (at ${absPath})`);
477
+ }
478
+ }
479
+ }
480
+ if (obj.initialHeadCommits !== undefined) {
481
+ if (typeof obj.initialHeadCommits !== "object" || obj.initialHeadCommits === null || Array.isArray(obj.initialHeadCommits)) {
482
+ throw new Error(`RuntimeTaskContext field "initialHeadCommits" must be an object when present (at ${absPath})`);
483
+ }
484
+ const headCommits = obj.initialHeadCommits;
485
+ for (const key of ["project", "monorepo"]) {
486
+ if (headCommits[key] === undefined) {
487
+ continue;
488
+ }
489
+ if (typeof headCommits[key] !== "string") {
490
+ throw new Error(`RuntimeTaskContext field "initialHeadCommits.${key}" must be a string when present (at ${absPath})`);
491
+ }
686
492
  }
687
- const dir = resolve6(skillsRoot, skillDirName(skill.id));
688
- mkdirSync3(dir, { recursive: true });
689
- writeFileSync3(resolve6(dir, "SKILL.md"), body, "utf-8");
690
- writeFileSync3(resolve6(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
691
- `, "utf-8");
692
- written.push({ id: skill.id, pluginName, directory: dir });
693
493
  }
694
- return written;
494
+ return obj;
695
495
  }
696
-
697
- // packages/runtime/src/control-plane/plugin-host-context.ts
698
- async function buildPluginHostContext(projectRoot) {
699
- let config;
700
- try {
701
- config = await loadConfig(projectRoot);
702
- } catch (err) {
703
- const msg = err instanceof Error ? err.message : String(err);
704
- if (msg.includes("no rig.config")) {
705
- return null;
706
- }
707
- throw err;
496
+ function loadRuntimeContextFromEnv(env = process.env) {
497
+ const contextFile = env[RUNTIME_CONTEXT_ENV];
498
+ if (contextFile) {
499
+ return loadRuntimeContext(contextFile);
708
500
  }
709
- const pluginHost = createPluginHost(config.plugins);
710
- setScopeRules(config.workspace.scopeNormalization);
711
- const validatorRegistry = createValidatorRegistry();
712
- for (const impl of pluginHost.listExecutableValidators()) {
713
- validatorRegistry.register(impl);
501
+ const inferred = findRuntimeContextFile(process.cwd());
502
+ if (!inferred) {
503
+ return null;
714
504
  }
715
- const taskSourceRegistry = buildTaskSourceRegistry(config, pluginHost);
716
- const repoRegistry = createRepoRegistry(pluginHost.listRepoSources());
717
- const managedEntries = pluginHost.listRepoSources().map(repoRegistrationToManagedEntry).filter((e) => e !== null);
718
- setManagedRepos(managedEntries);
719
- const configRoleOverrides = config.runtime?.agentRoles;
720
- const agentRoleRegistry = createAgentRoleRegistry(pluginHost.listAgentRoles(), configRoleOverrides);
721
- const taskFieldRegistry = createTaskFieldRegistry(pluginHost.listTaskFieldExtensions());
722
- try {
723
- const hookEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.hooks ?? []).map((hook) => ({
724
- pluginName: plugin.name,
725
- hook
726
- })));
727
- if (hookEntries.length > 0) {
728
- materializeHooks(projectRoot, hookEntries);
505
+ return loadRuntimeContext(inferred);
506
+ }
507
+ function findRuntimeContextFile(startPath) {
508
+ let current = resolve2(startPath);
509
+ while (true) {
510
+ const candidate = resolve2(current, "runtime-context.json");
511
+ if (existsSync2(candidate) && isAgentRuntimeContextPath(candidate)) {
512
+ return candidate;
729
513
  }
730
- } catch (err) {
731
- console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
732
- }
733
- try {
734
- const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
735
- pluginName: plugin.name,
736
- skill
737
- })));
738
- if (skillEntries.length > 0) {
739
- await materializeSkills(projectRoot, skillEntries);
514
+ const parent = dirname2(current);
515
+ if (parent === current) {
516
+ return "";
740
517
  }
741
- } catch (err) {
742
- console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
518
+ current = parent;
743
519
  }
744
- return {
745
- config,
746
- pluginHost,
747
- validatorRegistry,
748
- taskSourceRegistry,
749
- repoRegistry,
750
- agentRoleRegistry,
751
- taskFieldRegistry
752
- };
753
520
  }
754
-
755
- // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
756
- import { spawnSync } from "child_process";
757
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
758
- import { basename as basename3, join as join2, resolve as resolve8 } from "path";
759
-
760
- // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
761
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
762
- import { resolve as resolve7 } from "path";
763
-
764
- // packages/runtime/src/control-plane/tasks/task-record-reader.ts
765
- async function findTaskById(reader, id) {
766
- const tasks = await reader.listTasks();
767
- return tasks.find((task) => task.id === id) ?? null;
521
+ function isAgentRuntimeContextPath(path) {
522
+ const normalized = path.replace(/\\/g, "/");
523
+ return /\/\.rig\/runtime-context\.json$/.test(normalized);
768
524
  }
769
525
 
770
- // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
771
- class LegacyTaskConfigReadError extends Error {
772
- code = "LEGACY_TASK_CONFIG_READ_FAILED";
773
- projectRoot;
774
- configPath;
775
- cause;
776
- constructor(input) {
777
- super(input.message, { cause: input.cause });
778
- this.name = "LegacyTaskConfigReadError";
779
- this.projectRoot = input.projectRoot;
780
- this.configPath = input.configPath;
781
- this.cause = input.cause;
782
- }
526
+ // packages/runtime/src/control-plane/runtime/tooling/shell.ts
527
+ import { tmpdir as tmpdir2 } from "os";
528
+ import { basename, dirname as dirname3, resolve as resolve3 } from "path";
529
+ var sharedNativeShellOutputDir = resolve3(tmpdir2(), "rig-native");
530
+ var sharedNativeShellOutputPath = resolve3(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
531
+ function runtimeToolGatewayNames() {
532
+ return [
533
+ "bash",
534
+ "sh",
535
+ "zsh",
536
+ "git",
537
+ "bun",
538
+ "node",
539
+ "python3",
540
+ "rg",
541
+ "grep",
542
+ "sed",
543
+ "cat",
544
+ "ls",
545
+ "find",
546
+ "tsc",
547
+ "gh",
548
+ "mkdir",
549
+ "rm",
550
+ "mv",
551
+ "cp",
552
+ "touch",
553
+ "pwd",
554
+ "head",
555
+ "tail",
556
+ "wc",
557
+ "sort",
558
+ "uniq",
559
+ "awk",
560
+ "xargs",
561
+ "dirname",
562
+ "basename",
563
+ "realpath",
564
+ "env",
565
+ "jq",
566
+ "tee",
567
+ "which"
568
+ ];
783
569
  }
784
- function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
785
- const configPath = options.configPath ?? resolve7(projectRoot, ".rig", "task-config.json");
786
- const reader = {
787
- async listTasks() {
788
- return readLegacyTaskRecords(projectRoot, configPath);
789
- },
790
- async getTask(id) {
791
- return findTaskById(reader, id);
792
- }
793
- };
794
- return reader;
570
+ // packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
571
+ import { tmpdir as tmpdir3 } from "os";
572
+ import { basename as basename2, dirname as dirname4, resolve as resolve4 } from "path";
573
+ var sharedNativeToolsOutputDir = resolve4(tmpdir3(), "rig-native");
574
+ var sharedNativeToolsOutputPath = resolve4(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
575
+ function runtimeFileToolNames() {
576
+ return [
577
+ "rig-read",
578
+ "rig-write",
579
+ "rig-edit",
580
+ "rig-glob",
581
+ "rig-grep"
582
+ ];
795
583
  }
796
- function readLegacyTaskRecords(projectRoot, configPath = resolve7(projectRoot, ".rig", "task-config.json")) {
797
- if (!existsSync5(configPath)) {
798
- return [];
799
- }
800
- const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
801
- return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
584
+
585
+ // packages/runtime/src/control-plane/runtime/tooling/gateway.ts
586
+ function runtimeGatewayToolNames() {
587
+ return runtimeToolGatewayNames();
802
588
  }
803
- function readLegacyTaskConfigJson(projectRoot, configPath) {
804
- try {
805
- const parsed = JSON.parse(readFileSync4(configPath, "utf8"));
806
- if (isPlainRecord(parsed)) {
807
- return parsed;
808
- }
809
- throw new Error("task config root must be a JSON object");
810
- } catch (cause) {
811
- throw new LegacyTaskConfigReadError({
812
- projectRoot,
813
- configPath,
814
- message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
815
- cause
816
- });
589
+ // packages/runtime/src/control-plane/browser-contract.ts
590
+ import { resolve as resolve5 } from "path";
591
+ var DEFAULT_BROWSER_ATTACH_URL = "http://127.0.0.1:9333";
592
+ var DEFAULT_BROWSER_MODE = "persistent";
593
+ var RUNTIME_BROWSER_HELPERS = {
594
+ launch: "rig-browser-launch",
595
+ check: "rig-browser-check",
596
+ attachInfo: "rig-browser-attach-info",
597
+ e2e: "rig-browser-e2e",
598
+ resetProfile: "rig-browser-reset-profile"
599
+ };
600
+ var BASE_REMOTE_DEBUGGING_PORT = 9222;
601
+ var REMOTE_DEBUGGING_PORT_SPREAD = 4000;
602
+ function hashString(input) {
603
+ let hash = 0;
604
+ for (let index = 0;index < input.length; index += 1) {
605
+ hash = (hash << 5) - hash + input.charCodeAt(index) | 0;
817
606
  }
607
+ return Math.abs(hash);
818
608
  }
819
- function stripLegacyTaskConfigMetadata(raw) {
820
- const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
821
- return tasks;
822
- }
823
- function legacyTaskConfigEntryToRecord(id, entry) {
824
- if (!isPlainRecord(entry)) {
825
- return null;
826
- }
827
- const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
828
- const validation = readStringList(entry.validation);
829
- const validators = readStringList(entry.validators);
830
- const scope = readStringList(entry.scope);
831
- const status = typeof entry.status === "string" ? entry.status : "open";
832
- const title = typeof entry.title === "string" ? entry.title : undefined;
833
- const description = typeof entry.description === "string" ? entry.description : undefined;
834
- const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
835
- return {
836
- id,
837
- deps,
838
- status,
839
- source: "legacy-task-config",
840
- ...title ? { title } : {},
841
- ...description ? { description } : {},
842
- ...acceptanceCriteria ? { acceptanceCriteria } : {},
843
- ...scope.length > 0 ? { scope } : {},
844
- ...validation.length > 0 ? { validation } : {},
845
- ...validators.length > 0 ? { validators } : {},
846
- ...preservedLegacyFields(entry)
847
- };
609
+ function derivePortFromProfile(profileName) {
610
+ return BASE_REMOTE_DEBUGGING_PORT + hashString(profileName) % REMOTE_DEBUGGING_PORT_SPREAD;
848
611
  }
849
- function preservedLegacyFields(entry) {
850
- const preserved = {};
851
- for (const key of [
852
- "role",
853
- "browser",
854
- "repo_pins",
855
- "criticality",
856
- "queue_weight",
857
- "creates_repo",
858
- "auto_synced"
859
- ]) {
860
- if (entry[key] !== undefined) {
861
- preserved[key] = entry[key];
862
- }
612
+ function parseAttachUrl(attachUrl) {
613
+ try {
614
+ return new URL((attachUrl || DEFAULT_BROWSER_ATTACH_URL).trim() || DEFAULT_BROWSER_ATTACH_URL);
615
+ } catch {
616
+ return new URL(DEFAULT_BROWSER_ATTACH_URL);
863
617
  }
864
- return preserved;
865
618
  }
866
- function firstStringList(...candidates) {
867
- for (const candidate of candidates) {
868
- const list = readStringList(candidate);
869
- if (list.length > 0) {
870
- return list;
871
- }
619
+ function sanitizeRuntimeSuffix(runtimeId) {
620
+ return runtimeId.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 16) || "runtime";
621
+ }
622
+ function resolveBrowserStateDir(projectRoot, configuredStateDir) {
623
+ const trimmed = configuredStateDir?.trim() || ".tmp/rig-browser";
624
+ if (trimmed.startsWith("/")) {
625
+ return resolve5(trimmed);
872
626
  }
873
- return [];
627
+ return resolve5(projectRoot || process.cwd(), trimmed);
874
628
  }
875
- function readStringList(candidate) {
876
- if (!Array.isArray(candidate)) {
877
- return [];
629
+ function resolveTaskBrowserContext(browser, options = {}) {
630
+ if (!browser?.required) {
631
+ return;
878
632
  }
879
- return candidate.filter((value) => typeof value === "string");
633
+ const defaultProfile = browser.profile?.trim() || "default";
634
+ const mode = browser.mode?.trim() || DEFAULT_BROWSER_MODE;
635
+ const defaultAttach = parseAttachUrl(browser.attach_url);
636
+ const shouldDeriveRuntimeProfile = Boolean(options.runtimeId?.trim()) && mode !== "shared";
637
+ const effectiveProfile = shouldDeriveRuntimeProfile ? `${defaultProfile}-${sanitizeRuntimeSuffix(options.runtimeId.trim())}` : defaultProfile;
638
+ const effectivePort = shouldDeriveRuntimeProfile ? derivePortFromProfile(effectiveProfile) : Number(defaultAttach.port || "80");
639
+ const effectiveAttach = new URL(defaultAttach.toString());
640
+ effectiveAttach.port = String(effectivePort);
641
+ return {
642
+ required: true,
643
+ preset: browser.preset?.trim() || "default",
644
+ mode,
645
+ stateDir: resolveBrowserStateDir(options.hostProjectRoot, browser.state_dir),
646
+ defaultProfile,
647
+ effectiveProfile,
648
+ defaultAttachUrl: defaultAttach.toString(),
649
+ effectiveAttachUrl: effectiveAttach.toString(),
650
+ devCommand: browser.dev_command?.trim() || undefined,
651
+ launchCommand: browser.launch_command?.trim() || undefined,
652
+ checkCommand: browser.check_command?.trim() || undefined,
653
+ e2eCommand: browser.e2e_command?.trim() || undefined,
654
+ launchHelper: RUNTIME_BROWSER_HELPERS.launch,
655
+ checkHelper: RUNTIME_BROWSER_HELPERS.check,
656
+ attachInfoHelper: RUNTIME_BROWSER_HELPERS.attachInfo,
657
+ e2eHelper: RUNTIME_BROWSER_HELPERS.e2e,
658
+ resetProfileHelper: RUNTIME_BROWSER_HELPERS.resetProfile
659
+ };
880
660
  }
881
- function isPlainRecord(candidate) {
882
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
661
+ function buildBrowserGuidanceLines(browser) {
662
+ const lines = [
663
+ "This task requires Rig Browser.",
664
+ `Launch the browser: \`${browser.launchHelper}\`${browser.devCommand ? " or `rig-browser-launch --dev`" : ""}.`,
665
+ `Check the browser contract: \`${browser.checkHelper}\`.`,
666
+ `Show attach details: \`${browser.attachInfoHelper}\`.`,
667
+ `Attach Chrome DevTools MCP to ${browser.effectiveAttachUrl}.`,
668
+ `Preset: ${browser.preset}.`,
669
+ `Profile: ${browser.effectiveProfile}.`,
670
+ `State dir: ${browser.stateDir}.`,
671
+ `Reset the active profile with \`${browser.resetProfileHelper}\`.`
672
+ ];
673
+ if (browser.e2eCommand) {
674
+ lines.push(`Run app-owned browser e2e with \`${browser.e2eHelper}\`.`);
675
+ }
676
+ if (browser.defaultProfile !== browser.effectiveProfile) {
677
+ lines.push(`Base profile: ${browser.defaultProfile}. Runtime launches derive an isolated effective profile.`);
678
+ }
679
+ return lines;
883
680
  }
884
681
 
885
- // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
886
- var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
887
- var FILE_TASK_PATTERN = /\.(task\.)?json$/;
888
- function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
889
- const configPath = options.configPath ?? resolve8(projectRoot, ".rig", "task-config.json");
890
- const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
891
- const spawnFn = options.spawn ?? spawnSync;
892
- const ghBinary = options.ghBinary ?? "gh";
893
- const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
682
+ // packages/runtime/src/control-plane/plugin-host-context.ts
683
+ import { createPluginHost } from "@rig/core";
684
+ import { loadConfig } from "@rig/core/load-config";
685
+
686
+ // packages/runtime/src/control-plane/task-source.ts
687
+ function createTaskSourceRegistry() {
688
+ const byId = new Map;
689
+ const order = [];
894
690
  return {
895
- async listTasks() {
896
- const rawConfig = readRawTaskConfig(configPath);
897
- if (!rawConfig) {
898
- const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
899
- return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
900
- }
901
- const tasks = [];
902
- const legacyTasks = await legacy.listTasks();
903
- const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
904
- for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
905
- if (!isPlainRecord2(rawEntry)) {
906
- continue;
907
- }
908
- const metadata = readMaterializedTaskMetadata(rawEntry);
909
- if (metadata.taskSource?.kind === "github-issues") {
910
- tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
911
- continue;
912
- }
913
- if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
914
- const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
915
- if (fileTask)
916
- tasks.push(fileTask);
917
- continue;
918
- }
919
- if (!allowLocalFallback) {
920
- continue;
921
- }
922
- const legacyTask = legacyById.get(id);
923
- if (legacyTask) {
924
- tasks.push(legacyTask);
925
- }
926
- }
927
- return tasks;
691
+ register(s) {
692
+ if (byId.has(s.id))
693
+ throw new Error(`task source already registered: ${s.id}`);
694
+ byId.set(s.id, s);
695
+ order.push(s);
928
696
  },
929
- async getTask(id) {
930
- const rawEntry = readRawTaskEntry(configPath, id);
931
- if (!rawEntry) {
932
- const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
933
- return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
934
- }
935
- const metadata = readMaterializedTaskMetadata(rawEntry);
936
- if (metadata.taskSource?.kind === "github-issues") {
937
- return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
938
- }
939
- if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
940
- return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
941
- }
942
- return allowLocalFallback ? legacy.getTask(id) : null;
943
- }
697
+ resolveById(id) {
698
+ const s = byId.get(id);
699
+ if (!s)
700
+ throw new Error(`task source not registered: ${id}`);
701
+ return s;
702
+ },
703
+ resolveByKind(kind) {
704
+ for (const s of order)
705
+ if (s.kind === kind)
706
+ return s;
707
+ throw new Error(`no task source registered for kind: ${kind}`);
708
+ },
709
+ list: () => order
944
710
  };
945
711
  }
946
- function readMaterializedTaskMetadata(entry) {
947
- const rawRig = entry._rig;
948
- if (!isPlainRecord2(rawRig)) {
949
- return {};
950
- }
951
- const rawSource = rawRig.taskSource;
952
- const metadata = {};
953
- if (isPlainRecord2(rawSource)) {
954
- const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
955
- if (kind.length > 0) {
956
- metadata.taskSource = {
957
- kind,
958
- ...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
959
- ...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
960
- ...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
961
- ...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
962
- ...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
963
- };
964
- }
965
- }
966
- if (typeof rawRig.sourceIssueId === "string") {
967
- metadata.sourceIssueId = rawRig.sourceIssueId;
968
- }
969
- return metadata;
970
- }
971
- function readConfiguredFilesTaskSourcePath(projectRoot) {
972
- const jsonPath = resolve8(projectRoot, "rig.config.json");
973
- if (existsSync6(jsonPath)) {
974
- try {
975
- const parsed = JSON.parse(readFileSync5(jsonPath, "utf8"));
976
- if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
977
- const source = parsed.taskSource;
978
- return source.kind === "files" && typeof source.path === "string" ? source.path : null;
979
- }
980
- } catch {
981
- return null;
982
- }
983
- }
984
- const tsPath = resolve8(projectRoot, "rig.config.ts");
985
- if (!existsSync6(tsPath)) {
986
- return null;
987
- }
988
- try {
989
- const source = readFileSync5(tsPath, "utf8");
990
- const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
991
- const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
992
- if (kind !== "files") {
993
- return null;
994
- }
995
- return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
996
- } catch {
997
- return null;
998
- }
712
+
713
+ // packages/runtime/src/control-plane/task-source-bootstrap.ts
714
+ function formatRegisteredKinds(pluginHost) {
715
+ const kinds = pluginHost ? pluginHost.listExecutableTaskSources().map((source) => source.kind) : [];
716
+ return kinds.length > 0 ? kinds.join(", ") : "none";
999
717
  }
1000
- function readRawTaskEntry(configPath, taskId) {
1001
- const rawConfig = readRawTaskConfig(configPath);
1002
- if (!rawConfig) {
1003
- return null;
718
+ function buildTaskSourceRegistry(config, pluginHost) {
719
+ const registry = createTaskSourceRegistry();
720
+ const taskSourceConfig = config.taskSource;
721
+ const factory = pluginHost?.resolveTaskSourceFactoryByKind(taskSourceConfig.kind);
722
+ if (!factory) {
723
+ throw new Error(`No task source factory registered for kind "${taskSourceConfig.kind}". ` + `Registered kinds: ${formatRegisteredKinds(pluginHost)}. ` + "Load a plugin that contributes an executable task source factory for this kind.");
1004
724
  }
1005
- const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
1006
- return isPlainRecord2(entry) ? entry : null;
725
+ registry.register(factory.factory(taskSourceConfig));
726
+ return registry;
1007
727
  }
1008
- function readRawTaskConfig(configPath) {
1009
- if (!existsSync6(configPath)) {
1010
- return null;
728
+
729
+ // packages/runtime/src/control-plane/repos/registry.ts
730
+ function createRepoRegistry(entries) {
731
+ const map = new Map;
732
+ for (const e of entries) {
733
+ if (map.has(e.id))
734
+ throw new Error(`repo already registered: ${e.id}`);
735
+ map.set(e.id, { ...e });
1011
736
  }
1012
- const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
1013
- return isPlainRecord2(parsed) ? parsed : null;
1014
- }
1015
- function stripLegacyTaskConfigMetadata2(raw) {
1016
- const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1017
- return tasks;
737
+ const ordered = Array.from(map.values());
738
+ return {
739
+ getById: (id) => map.get(id),
740
+ list: () => ordered
741
+ };
1018
742
  }
1019
- function listFileBackedTasks(projectRoot, sourcePath) {
1020
- const directory = resolve8(projectRoot, sourcePath);
1021
- if (!existsSync6(directory)) {
1022
- return [];
1023
- }
1024
- const tasks = [];
1025
- for (const name of readdirSync2(directory)) {
1026
- if (!FILE_TASK_PATTERN.test(name))
1027
- continue;
1028
- const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
1029
- const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
1030
- if (task)
1031
- tasks.push(task);
743
+ var MANAGED_REPOS = new Map;
744
+ function setManagedRepos(entries) {
745
+ const next = new Map;
746
+ for (const e of entries) {
747
+ if (next.has(e.id)) {
748
+ throw new Error(`managed repo already registered: ${e.id}`);
749
+ }
750
+ next.set(e.id, e);
1032
751
  }
1033
- return tasks;
752
+ MANAGED_REPOS = next;
1034
753
  }
1035
- function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
1036
- const file = findFileBackedTaskFile(resolve8(projectRoot, sourcePath), taskId);
1037
- if (!file) {
1038
- return null;
754
+ function getManagedRepoEntry(repoId) {
755
+ const entry = MANAGED_REPOS.get(repoId);
756
+ if (!entry) {
757
+ throw new Error(`managed repo not registered: ${repoId}. Plugins contribute repos via RigPlugin.contributes.repoSources; ` + `make sure a plugin declares this id and the plugin host has been initialized.`);
1039
758
  }
1040
- const raw = JSON.parse(readFileSync5(file, "utf8"));
1041
- if (!isPlainRecord2(raw)) {
759
+ return entry;
760
+ }
761
+ function listManagedRepoEntries() {
762
+ return Array.from(MANAGED_REPOS.values());
763
+ }
764
+ function repoRegistrationToManagedEntry(reg) {
765
+ if (!reg.defaultBranch) {
1042
766
  return null;
1043
767
  }
1044
768
  return {
1045
- id: typeof raw.id === "string" ? raw.id : taskId,
1046
- deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
1047
- status: typeof raw.status === "string" ? raw.status : "ready",
1048
- title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
1049
- ...raw
769
+ id: reg.id,
770
+ alias: reg.defaultPath ?? reg.id,
771
+ defaultBranch: reg.defaultBranch,
772
+ defaultRemoteUrl: reg.url,
773
+ remoteEnvVar: reg.remoteEnvVar,
774
+ checkoutEnvVar: reg.checkoutEnvVar
1050
775
  };
1051
776
  }
1052
- function findFileBackedTaskFile(directory, taskId) {
1053
- if (!existsSync6(directory)) {
1054
- return null;
777
+
778
+ // packages/runtime/src/control-plane/agent-roles.ts
779
+ function createAgentRoleRegistry(pluginRoles, configOverrides) {
780
+ const map = new Map;
781
+ for (const r of pluginRoles) {
782
+ if (map.has(r.id))
783
+ throw new Error(`agent role already registered: ${r.id}`);
784
+ const override = configOverrides?.[r.id];
785
+ const model = override?.model ?? r.defaultModel;
786
+ if (!model) {
787
+ throw new Error(`agent role "${r.id}" has no model \u2014 provide defaultModel in plugin or model in config.runtime.agentRoles.${r.id}`);
788
+ }
789
+ map.set(r.id, { ...r, model });
1055
790
  }
1056
- for (const name of readdirSync2(directory)) {
1057
- if (!FILE_TASK_PATTERN.test(name))
1058
- continue;
1059
- const file = join2(directory, name);
1060
- try {
1061
- if (!statSync(file).isFile())
1062
- continue;
1063
- const raw = JSON.parse(readFileSync5(file, "utf8"));
1064
- const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
1065
- const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
1066
- if (id === taskId) {
1067
- return file;
1068
- }
1069
- } catch {}
791
+ return {
792
+ resolve(id) {
793
+ const r = map.get(id);
794
+ if (!r)
795
+ throw new Error(`agent role not registered: ${id}`);
796
+ return r;
797
+ },
798
+ list: () => Array.from(map.values())
799
+ };
800
+ }
801
+
802
+ // packages/runtime/src/control-plane/task-fields.ts
803
+ function createTaskFieldRegistry(extensions) {
804
+ const byId = new Map;
805
+ for (const e of extensions) {
806
+ if (byId.has(e.id))
807
+ throw new Error(`task field extension already registered: ${e.id}`);
808
+ byId.set(e.id, e);
1070
809
  }
1071
- return null;
810
+ return {
811
+ get: (id) => byId.get(id),
812
+ list: () => Array.from(byId.values()),
813
+ fieldNames: () => Array.from(byId.values()).map((e) => e.fieldName),
814
+ validateTaskFields(task) {
815
+ const errors = [];
816
+ for (const ext of byId.values()) {
817
+ let schema;
818
+ try {
819
+ schema = JSON.parse(ext.schemaJson);
820
+ } catch {
821
+ errors.push(`task field "${ext.id}": schemaJson is not valid JSON`);
822
+ continue;
823
+ }
824
+ const isRequired = typeof schema === "object" && schema !== null && schema.required === true;
825
+ if (!isRequired)
826
+ continue;
827
+ const value = task[ext.fieldName];
828
+ if (value === undefined || value === null || value === "") {
829
+ errors.push(`task field "${ext.fieldName}" (from extension "${ext.id}") is required but missing`);
830
+ }
831
+ }
832
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
833
+ }
834
+ };
1072
835
  }
1073
- function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
1074
- const source = requireGithubIssueSource(metadata, id);
1075
- const issue = runGh(bin, [
1076
- "issue",
1077
- "view",
1078
- String(id),
1079
- "--repo",
1080
- `${source.owner}/${source.repo}`,
1081
- "--json",
1082
- "number,title,body,labels,state,url,assignees"
1083
- ], spawnFn);
1084
- return githubIssueToTask(issue, source, rawEntry);
836
+
837
+ // packages/runtime/src/control-plane/validators/runtime-registration.ts
838
+ import { existsSync as existsSync3 } from "fs";
839
+ import { join } from "path";
840
+ function createValidatorRegistry() {
841
+ const map = new Map;
842
+ const order = [];
843
+ const registry = {
844
+ register(v) {
845
+ if (map.has(v.id))
846
+ throw new Error(`validator already registered: ${v.id}`);
847
+ map.set(v.id, v);
848
+ order.push(v);
849
+ },
850
+ resolve(id) {
851
+ const v = map.get(id);
852
+ if (!v)
853
+ throw new Error(`validator not registered: ${id}`);
854
+ return v;
855
+ },
856
+ list: () => order
857
+ };
858
+ registerBuiltInValidators(registry);
859
+ return registry;
1085
860
  }
1086
- function requireGithubIssueSource(metadata, id) {
1087
- const source = metadata.taskSource;
1088
- if (source?.kind === "github-issues" && source.owner && source.repo) {
1089
- return { owner: source.owner, repo: source.repo };
1090
- }
1091
- const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
1092
- if (parsed && parsed[3] === id) {
1093
- return { owner: parsed[1], repo: parsed[2] };
1094
- }
1095
- throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
861
+ function registerBuiltInValidators(registry) {
862
+ registry.register({
863
+ id: "std:typecheck",
864
+ category: "custom",
865
+ description: "Runs the package typecheck script when present.",
866
+ run: async (ctx) => runStdTypecheck(ctx)
867
+ });
1096
868
  }
1097
- function githubIssueToTask(issue, source, rawEntry) {
1098
- const labelNames = (issue.labels ?? []).map((label) => label.name);
1099
- const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
1100
- const roleLabel = labelNames.find((label) => label.startsWith("role:"));
1101
- const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
1102
- const body = issue.body ?? "";
1103
- const repo = `${source.owner}/${source.repo}`;
869
+ async function runStdTypecheck(ctx) {
870
+ const packageJsonPath = join(ctx.workspaceRoot, "package.json");
871
+ if (!existsSync3(packageJsonPath)) {
872
+ return {
873
+ id: "std:typecheck",
874
+ passed: false,
875
+ summary: `package.json not found at ${packageJsonPath}`
876
+ };
877
+ }
878
+ const proc = Bun.spawn(["bun", "run", "typecheck"], {
879
+ cwd: ctx.workspaceRoot,
880
+ env: process.env,
881
+ stdout: "pipe",
882
+ stderr: "pipe"
883
+ });
884
+ const [exitCode, stdout, stderr] = await Promise.all([
885
+ proc.exited,
886
+ new Response(proc.stdout).text(),
887
+ new Response(proc.stderr).text()
888
+ ]);
889
+ const output = `${stdout}${stderr}`.trim();
1104
890
  return {
1105
- id: String(issue.number),
1106
- deps: parseDeps(body),
1107
- status: githubStatusFor(issue),
1108
- title: issue.title,
1109
- body,
1110
- ...scope.length > 0 ? { scope } : {},
1111
- ...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
1112
- ...validators.length > 0 ? { validators } : {},
1113
- ...issue.url ? { url: issue.url } : {},
1114
- issueType: issueTypeFor(labelNames),
1115
- sourceIssueId: `${repo}#${issue.number}`,
1116
- parentChildDeps: parseParents(body),
1117
- labels: labelNames,
1118
- raw: issue,
1119
- source: "github-issues",
1120
- _rig: {
1121
- taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
1122
- sourceIssueId: `${repo}#${issue.number}`
1123
- }
891
+ id: "std:typecheck",
892
+ passed: exitCode === 0,
893
+ summary: exitCode === 0 ? "typecheck passed" : "typecheck failed",
894
+ ...output ? { details: output.slice(0, 4000) } : {}
1124
895
  };
1125
896
  }
1126
- function githubStatusFor(issue) {
1127
- const state = (issue.state ?? "").toUpperCase();
1128
- if (state === "CLOSED")
1129
- return "closed";
1130
- const labelNames = (issue.labels ?? []).map((label) => label.name);
1131
- if (labelNames.includes("in-progress"))
1132
- return "in_progress";
1133
- if (labelNames.includes("blocked"))
1134
- return "blocked";
1135
- if (labelNames.includes("ready"))
1136
- return "ready";
1137
- if (labelNames.includes("under-review"))
1138
- return "under_review";
1139
- if (labelNames.includes("failed"))
1140
- return "failed";
1141
- if (labelNames.includes("cancelled"))
1142
- return "cancelled";
1143
- return "open";
897
+
898
+ // packages/runtime/src/control-plane/native/scope-rules.ts
899
+ var activeRules = null;
900
+ function setScopeRules(rules) {
901
+ activeRules = rules ?? null;
1144
902
  }
1145
- function selectedGitHubEnv() {
1146
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1147
- return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
903
+ function getScopeRules() {
904
+ return activeRules;
1148
905
  }
1149
- function ghSpawnOptions() {
1150
- return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
906
+
907
+ // packages/runtime/src/control-plane/hook-materializer.ts
908
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
909
+ import { dirname as dirname5, resolve as resolve6 } from "path";
910
+ var MARKER_PLUGIN = "_rigPlugin";
911
+ var MARKER_HOOK_ID = "_rigHookId";
912
+ function matcherToString(matcher) {
913
+ if (matcher.kind === "all")
914
+ return;
915
+ if (matcher.kind === "tool")
916
+ return matcher.name;
917
+ return matcher.pattern;
1151
918
  }
1152
- function runGh(bin, args, spawnFn) {
1153
- const res = spawnFn(bin, [...args], ghSpawnOptions());
1154
- assertGhSuccess(args, res);
1155
- if (!res.stdout || res.stdout.trim() === "") {
1156
- throw new Error(`gh ${args.join(" ")} returned empty stdout`);
919
+ function isPluginOwned(cmd) {
920
+ return typeof cmd[MARKER_PLUGIN] === "string";
921
+ }
922
+ function materializeHooks(projectRoot, entries) {
923
+ const settingsPath = resolve6(projectRoot, ".claude", "settings.json");
924
+ const existing = existsSync4(settingsPath) ? safeReadJson(settingsPath) : {};
925
+ const hooks = existing.hooks ?? {};
926
+ for (const event of Object.keys(hooks)) {
927
+ const groups = hooks[event] ?? [];
928
+ const cleaned = [];
929
+ for (const group of groups) {
930
+ const operatorHooks = group.hooks.filter((h) => !isPluginOwned(h));
931
+ if (operatorHooks.length > 0) {
932
+ cleaned.push({ ...group, hooks: operatorHooks });
933
+ }
934
+ }
935
+ if (cleaned.length > 0) {
936
+ hooks[event] = cleaned;
937
+ } else {
938
+ delete hooks[event];
939
+ }
1157
940
  }
1158
- return JSON.parse(res.stdout);
1159
- }
1160
- function assertGhSuccess(args, res) {
1161
- if (res.error) {
1162
- const msg = res.error.message ?? String(res.error);
1163
- throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
941
+ for (const { pluginName, hook } of entries) {
942
+ if (!hook.command) {
943
+ continue;
944
+ }
945
+ const event = hook.event;
946
+ const matcherString = matcherToString(hook.matcher);
947
+ const groups = hooks[event] ??= [];
948
+ let group = groups.find((g) => g.matcher === matcherString);
949
+ if (!group) {
950
+ group = matcherString === undefined ? { hooks: [] } : { matcher: matcherString, hooks: [] };
951
+ groups.push(group);
952
+ }
953
+ group.hooks.push({
954
+ type: "command",
955
+ command: hook.command,
956
+ [MARKER_PLUGIN]: pluginName,
957
+ [MARKER_HOOK_ID]: hook.id
958
+ });
1164
959
  }
1165
- if (res.status !== 0) {
1166
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
960
+ const next = { ...existing };
961
+ if (Object.keys(hooks).length > 0) {
962
+ next.hooks = hooks;
963
+ } else {
964
+ delete next.hooks;
1167
965
  }
966
+ mkdirSync3(dirname5(settingsPath), { recursive: true });
967
+ writeFileSync3(settingsPath, `${JSON.stringify(next, null, 2)}
968
+ `, "utf-8");
969
+ return settingsPath;
1168
970
  }
1169
- function parseDeps(body) {
1170
- return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
1171
- }
1172
- function parseParents(body) {
1173
- return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
1174
- }
1175
- function parseIssueRefs(body, pattern) {
1176
- const match = body.match(pattern);
1177
- if (!match)
1178
- return [];
1179
- return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
1180
- }
1181
- function issueTypeFor(labels) {
1182
- const typed = labels.find((label) => label.startsWith("type:"));
1183
- if (typed)
1184
- return typed.slice("type:".length);
1185
- if (labels.includes("epic"))
1186
- return "epic";
1187
- return "task";
1188
- }
1189
- function isPlainRecord2(candidate) {
1190
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
971
+ function safeReadJson(path) {
972
+ try {
973
+ return JSON.parse(readFileSync3(path, "utf-8"));
974
+ } catch {
975
+ return {};
976
+ }
1191
977
  }
1192
978
 
1193
- // packages/runtime/src/control-plane/tasks/source-lifecycle.ts
1194
- function hasRunnableTaskSource(source) {
1195
- return Boolean(source && typeof source === "object" && !Array.isArray(source));
979
+ // packages/runtime/src/control-plane/skill-materializer.ts
980
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
981
+ import { resolve as resolve7 } from "path";
982
+ import { loadSkill } from "@rig/skill-loader";
983
+ var MARKER_FILENAME = ".rig-plugin";
984
+ function skillDirName(id) {
985
+ return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
1196
986
  }
1197
- async function getPluginTask(projectRoot, taskId) {
1198
- const ctx = await buildPluginHostContext(projectRoot);
1199
- const [source] = ctx?.taskSourceRegistry.list() ?? [];
1200
- if (!hasRunnableTaskSource(source)) {
1201
- return ctx ? { configured: false, sourceKind: null, task: null } : null;
987
+ async function materializeSkills(projectRoot, entries) {
988
+ const skillsRoot = resolve7(projectRoot, ".pi", "skills");
989
+ if (existsSync5(skillsRoot)) {
990
+ for (const name of readdirSync(skillsRoot)) {
991
+ const dir = resolve7(skillsRoot, name);
992
+ if (existsSync5(resolve7(dir, MARKER_FILENAME))) {
993
+ rmSync2(dir, { recursive: true, force: true });
994
+ }
995
+ }
1202
996
  }
1203
- const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
1204
- return {
1205
- configured: true,
1206
- sourceKind: source.kind,
1207
- task
1208
- };
997
+ const written = [];
998
+ for (const { pluginName, skill } of entries) {
999
+ const sourcePath = resolve7(projectRoot, skill.path);
1000
+ if (!existsSync5(sourcePath)) {
1001
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
1002
+ continue;
1003
+ }
1004
+ let body;
1005
+ try {
1006
+ await loadSkill(sourcePath);
1007
+ body = readFileSync4(sourcePath, "utf-8");
1008
+ } catch (err) {
1009
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
1010
+ continue;
1011
+ }
1012
+ const dir = resolve7(skillsRoot, skillDirName(skill.id));
1013
+ mkdirSync4(dir, { recursive: true });
1014
+ writeFileSync4(resolve7(dir, "SKILL.md"), body, "utf-8");
1015
+ writeFileSync4(resolve7(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
1016
+ `, "utf-8");
1017
+ written.push({ id: skill.id, pluginName, directory: dir });
1018
+ }
1019
+ return written;
1209
1020
  }
1210
- async function readConfiguredTaskSourceTask(projectRoot, taskId) {
1211
- const pluginResult = await getPluginTask(projectRoot, taskId);
1212
- if (pluginResult)
1213
- return pluginResult;
1214
- const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
1021
+
1022
+ // packages/runtime/src/control-plane/plugin-host-context.ts
1023
+ async function buildPluginHostContext(projectRoot) {
1024
+ let config;
1025
+ try {
1026
+ config = await loadConfig(projectRoot);
1027
+ } catch (err) {
1028
+ const msg = err instanceof Error ? err.message : String(err);
1029
+ if (msg.includes("no rig.config")) {
1030
+ return null;
1031
+ }
1032
+ throw err;
1033
+ }
1034
+ const pluginHost = createPluginHost(config.plugins);
1035
+ setScopeRules(config.workspace.scopeNormalization);
1036
+ const validatorRegistry = createValidatorRegistry();
1037
+ for (const impl of pluginHost.listExecutableValidators()) {
1038
+ validatorRegistry.register(impl);
1039
+ }
1040
+ const taskSourceRegistry = buildTaskSourceRegistry(config, pluginHost);
1041
+ const repoRegistry = createRepoRegistry(pluginHost.listRepoSources());
1042
+ const managedEntries = pluginHost.listRepoSources().map(repoRegistrationToManagedEntry).filter((e) => e !== null);
1043
+ setManagedRepos(managedEntries);
1044
+ const configRoleOverrides = config.runtime?.agentRoles;
1045
+ const agentRoleRegistry = createAgentRoleRegistry(pluginHost.listAgentRoles(), configRoleOverrides);
1046
+ const taskFieldRegistry = createTaskFieldRegistry(pluginHost.listTaskFieldExtensions());
1047
+ try {
1048
+ const hookEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.hooks ?? []).map((hook) => ({
1049
+ pluginName: plugin.name,
1050
+ hook
1051
+ })));
1052
+ if (hookEntries.length > 0) {
1053
+ materializeHooks(projectRoot, hookEntries);
1054
+ }
1055
+ } catch (err) {
1056
+ console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
1057
+ }
1058
+ try {
1059
+ const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
1060
+ pluginName: plugin.name,
1061
+ skill
1062
+ })));
1063
+ if (skillEntries.length > 0) {
1064
+ await materializeSkills(projectRoot, skillEntries);
1065
+ }
1066
+ } catch (err) {
1067
+ console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
1068
+ }
1215
1069
  return {
1216
- configured: false,
1217
- sourceKind: null,
1218
- task
1070
+ config,
1071
+ pluginHost,
1072
+ validatorRegistry,
1073
+ taskSourceRegistry,
1074
+ repoRegistry,
1075
+ agentRoleRegistry,
1076
+ taskFieldRegistry
1219
1077
  };
1220
1078
  }
1221
1079
 
1222
- // packages/runtime/src/control-plane/native/task-state.ts
1223
- import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
1224
- import { basename as basename6, resolve as resolve16 } from "path";
1080
+ // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
1081
+ import { spawnSync } from "child_process";
1082
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync5 } from "fs";
1083
+ import { basename as basename3, join as join2, resolve as resolve9 } from "path";
1225
1084
 
1226
- // packages/runtime/src/control-plane/state-sync/types.ts
1227
- var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
1228
- var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
1229
- "draft",
1230
- "open",
1231
- "ready",
1232
- "queued",
1233
- "in_progress",
1234
- "under_review",
1235
- "blocked",
1236
- "completed",
1237
- "cancelled"
1238
- ]);
1239
- function normalizeTaskLifecycleStatus(status) {
1240
- switch (status) {
1241
- case "draft":
1242
- case "open":
1243
- case "ready":
1244
- case "queued":
1245
- case "in_progress":
1246
- case "under_review":
1247
- case "blocked":
1248
- case "completed":
1249
- case "cancelled":
1250
- return status;
1251
- case "closed":
1252
- return "completed";
1253
- case "running":
1254
- return "in_progress";
1255
- case "failed":
1256
- return "ready";
1257
- default:
1258
- return null;
1259
- }
1085
+ // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
1086
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
1087
+ import { resolve as resolve8 } from "path";
1088
+
1089
+ // packages/runtime/src/control-plane/tasks/task-record-reader.ts
1090
+ async function findTaskById(reader, id) {
1091
+ const tasks = await reader.listTasks();
1092
+ return tasks.find((task) => task.id === id) ?? null;
1260
1093
  }
1261
- function normalizeTaskStateMetadataStatus(status) {
1262
- if (CANONICAL_TASK_LIFECYCLE_STATUSES.has(status)) {
1263
- return status;
1094
+
1095
+ // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
1096
+ class LegacyTaskConfigReadError extends Error {
1097
+ code = "LEGACY_TASK_CONFIG_READ_FAILED";
1098
+ projectRoot;
1099
+ configPath;
1100
+ cause;
1101
+ constructor(input) {
1102
+ super(input.message, { cause: input.cause });
1103
+ this.name = "LegacyTaskConfigReadError";
1104
+ this.projectRoot = input.projectRoot;
1105
+ this.configPath = input.configPath;
1106
+ this.cause = input.cause;
1264
1107
  }
1265
- switch (status) {
1266
- case "closed":
1267
- return "completed";
1268
- case "running":
1269
- return "in_progress";
1270
- case "failed":
1271
- return "ready";
1272
- default:
1273
- return;
1108
+ }
1109
+ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
1110
+ const configPath = options.configPath ?? resolve8(projectRoot, ".rig", "task-config.json");
1111
+ const reader = {
1112
+ async listTasks() {
1113
+ return readLegacyTaskRecords(projectRoot, configPath);
1114
+ },
1115
+ async getTask(id) {
1116
+ return findTaskById(reader, id);
1117
+ }
1118
+ };
1119
+ return reader;
1120
+ }
1121
+ function readLegacyTaskRecords(projectRoot, configPath = resolve8(projectRoot, ".rig", "task-config.json")) {
1122
+ if (!existsSync6(configPath)) {
1123
+ return [];
1274
1124
  }
1125
+ const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
1126
+ return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
1275
1127
  }
1276
- function canonicalizeTaskStateMetadata(raw) {
1277
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1278
- return null;
1128
+ function readLegacyTaskConfigJson(projectRoot, configPath) {
1129
+ try {
1130
+ const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
1131
+ if (isPlainRecord(parsed)) {
1132
+ return parsed;
1133
+ }
1134
+ throw new Error("task config root must be a JSON object");
1135
+ } catch (cause) {
1136
+ throw new LegacyTaskConfigReadError({
1137
+ projectRoot,
1138
+ configPath,
1139
+ message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
1140
+ cause
1141
+ });
1279
1142
  }
1280
- const metadata = raw;
1281
- const claimId = typeof metadata.claimId === "string" && metadata.claimId.length > 0 ? metadata.claimId : undefined;
1282
- const status = normalizeTaskStateMetadataStatus(metadata.status);
1283
- if (!status) {
1143
+ }
1144
+ function stripLegacyTaskConfigMetadata(raw) {
1145
+ const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1146
+ return tasks;
1147
+ }
1148
+ function legacyTaskConfigEntryToRecord(id, entry) {
1149
+ if (!isPlainRecord(entry)) {
1284
1150
  return null;
1285
1151
  }
1152
+ const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
1153
+ const validation = readStringList(entry.validation);
1154
+ const validators = readStringList(entry.validators);
1155
+ const scope = readStringList(entry.scope);
1156
+ const status = typeof entry.status === "string" ? entry.status : "open";
1157
+ const title = typeof entry.title === "string" ? entry.title : undefined;
1158
+ const description = typeof entry.description === "string" ? entry.description : undefined;
1159
+ const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
1286
1160
  return {
1287
- ...claimId ? { claimId } : {},
1161
+ id,
1162
+ deps,
1288
1163
  status,
1289
- ...typeof metadata.ownerId === "string" && metadata.ownerId.length > 0 ? { ownerId: metadata.ownerId } : {},
1290
- ...typeof metadata.claimedAt === "string" && metadata.claimedAt.length > 0 ? { claimedAt: metadata.claimedAt } : {},
1291
- ...typeof metadata.lastEvidenceAt === "string" && metadata.lastEvidenceAt.length > 0 ? { lastEvidenceAt: metadata.lastEvidenceAt } : {},
1292
- ...typeof metadata.runId === "string" && metadata.runId.length > 0 ? { runId: metadata.runId } : {},
1293
- ...typeof metadata.branchName === "string" && metadata.branchName.length > 0 ? { branchName: metadata.branchName } : {},
1294
- ...typeof metadata.prNumber === "number" ? { prNumber: metadata.prNumber } : {},
1295
- ...typeof metadata.prUrl === "string" && metadata.prUrl.length > 0 ? { prUrl: metadata.prUrl } : {},
1296
- ...typeof metadata.reviewState === "string" && metadata.reviewState.length > 0 ? { reviewState: metadata.reviewState } : {},
1297
- ...typeof metadata.blockerReason === "string" && metadata.blockerReason.length > 0 ? { blockerReason: metadata.blockerReason } : {},
1298
- ...typeof metadata.sourceCommit === "string" && metadata.sourceCommit.length > 0 ? { sourceCommit: metadata.sourceCommit } : {}
1164
+ source: "legacy-task-config",
1165
+ ...title ? { title } : {},
1166
+ ...description ? { description } : {},
1167
+ ...acceptanceCriteria ? { acceptanceCriteria } : {},
1168
+ ...scope.length > 0 ? { scope } : {},
1169
+ ...validation.length > 0 ? { validation } : {},
1170
+ ...validators.length > 0 ? { validators } : {},
1171
+ ...preservedLegacyFields(entry)
1299
1172
  };
1300
1173
  }
1301
- function discardMismatchedTaskStateMetadata(input) {
1302
- input.taskId;
1303
- const canonicalMetadata = canonicalizeTaskStateMetadata(input.metadata);
1304
- if (!canonicalMetadata || !input.lifecycleStatus) {
1305
- return null;
1306
- }
1307
- const metadataStatus = canonicalMetadata.status ?? null;
1308
- if (metadataStatus && metadataStatus !== input.lifecycleStatus) {
1309
- return null;
1174
+ function preservedLegacyFields(entry) {
1175
+ const preserved = {};
1176
+ for (const key of [
1177
+ "role",
1178
+ "browser",
1179
+ "repo_pins",
1180
+ "criticality",
1181
+ "queue_weight",
1182
+ "creates_repo",
1183
+ "auto_synced"
1184
+ ]) {
1185
+ if (entry[key] !== undefined) {
1186
+ preserved[key] = entry[key];
1187
+ }
1310
1188
  }
1311
- return canonicalMetadata;
1189
+ return preserved;
1312
1190
  }
1313
- function readTaskStateMetadataEnvelope(raw) {
1314
- if (!raw || typeof raw !== "object") {
1315
- return { schemaVersion: SUPPORTED_TASK_STATE_SCHEMA_VERSION, supported: true, baseTrackerCommit: null, tasks: {} };
1191
+ function firstStringList(...candidates) {
1192
+ for (const candidate of candidates) {
1193
+ const list = readStringList(candidate);
1194
+ if (list.length > 0) {
1195
+ return list;
1196
+ }
1316
1197
  }
1317
- const envelope = raw;
1318
- const schemaVersion = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : SUPPORTED_TASK_STATE_SCHEMA_VERSION;
1319
- if (schemaVersion !== SUPPORTED_TASK_STATE_SCHEMA_VERSION) {
1320
- return { schemaVersion, supported: false, baseTrackerCommit: null, tasks: {} };
1198
+ return [];
1199
+ }
1200
+ function readStringList(candidate) {
1201
+ if (!Array.isArray(candidate)) {
1202
+ return [];
1321
1203
  }
1322
- const rawTasks = envelope.tasks && typeof envelope.tasks === "object" && !Array.isArray(envelope.tasks) ? envelope.tasks : {};
1323
- const tasks = Object.fromEntries(Object.entries(rawTasks).map(([taskId, metadata]) => [taskId, canonicalizeTaskStateMetadata(metadata)]).filter((entry) => entry[1] != null));
1324
- return {
1325
- schemaVersion,
1326
- supported: true,
1327
- baseTrackerCommit: typeof envelope.baseTrackerCommit === "string" && envelope.baseTrackerCommit.length > 0 ? envelope.baseTrackerCommit : null,
1328
- tasks
1329
- };
1204
+ return candidate.filter((value) => typeof value === "string");
1330
1205
  }
1331
- // packages/runtime/src/control-plane/state-sync/read.ts
1332
- import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
1333
- import { resolve as resolve15 } from "path";
1334
-
1335
- // packages/runtime/src/control-plane/native/git-native.ts
1336
- import { chmodSync, copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1337
- import { tmpdir as tmpdir3 } from "os";
1338
- import { dirname as dirname5, isAbsolute, resolve as resolve9 } from "path";
1339
- import { createHash } from "crypto";
1340
- function isTextTreeCommitUpdate(update) {
1341
- return typeof update.content === "string";
1206
+ function isPlainRecord(candidate) {
1207
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1342
1208
  }
1343
- var sharedGitNativeOutputDir = resolve9(tmpdir3(), "rig-native");
1344
- var sharedGitNativeOutputPath = resolve9(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
1345
- var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
1346
- function temporaryGitBinaryOutputPath(outputPath) {
1347
- const suffix = process.platform === "win32" ? ".exe" : "";
1348
- return resolve9(dirname5(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
1209
+
1210
+ // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
1211
+ var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
1212
+ var FILE_TASK_PATTERN = /\.(task\.)?json$/;
1213
+ function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
1214
+ const configPath = options.configPath ?? resolve9(projectRoot, ".rig", "task-config.json");
1215
+ const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
1216
+ const spawnFn = options.spawn ?? spawnSync;
1217
+ const ghBinary = options.ghBinary ?? "gh";
1218
+ const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
1219
+ return {
1220
+ async listTasks() {
1221
+ const rawConfig = readRawTaskConfig(configPath);
1222
+ if (!rawConfig) {
1223
+ const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1224
+ return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
1225
+ }
1226
+ const tasks = [];
1227
+ const legacyTasks = await legacy.listTasks();
1228
+ const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
1229
+ for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
1230
+ if (!isPlainRecord2(rawEntry)) {
1231
+ continue;
1232
+ }
1233
+ const metadata = readMaterializedTaskMetadata(rawEntry);
1234
+ if (metadata.taskSource?.kind === "github-issues") {
1235
+ tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
1236
+ continue;
1237
+ }
1238
+ if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1239
+ const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1240
+ if (fileTask)
1241
+ tasks.push(fileTask);
1242
+ continue;
1243
+ }
1244
+ if (!allowLocalFallback) {
1245
+ continue;
1246
+ }
1247
+ const legacyTask = legacyById.get(id);
1248
+ if (legacyTask) {
1249
+ tasks.push(legacyTask);
1250
+ }
1251
+ }
1252
+ return tasks;
1253
+ },
1254
+ async getTask(id) {
1255
+ const rawEntry = readRawTaskEntry(configPath, id);
1256
+ if (!rawEntry) {
1257
+ const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1258
+ return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
1259
+ }
1260
+ const metadata = readMaterializedTaskMetadata(rawEntry);
1261
+ if (metadata.taskSource?.kind === "github-issues") {
1262
+ return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
1263
+ }
1264
+ if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1265
+ return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1266
+ }
1267
+ return allowLocalFallback ? legacy.getTask(id) : null;
1268
+ }
1269
+ };
1349
1270
  }
1350
- function publishGitBinary(tempOutputPath, outputPath) {
1351
- try {
1352
- renameSync(tempOutputPath, outputPath);
1353
- } catch (error) {
1354
- if (process.platform === "win32" && existsSync7(outputPath)) {
1355
- rmSync2(outputPath, { force: true });
1356
- renameSync(tempOutputPath, outputPath);
1357
- return;
1271
+ function readMaterializedTaskMetadata(entry) {
1272
+ const rawRig = entry._rig;
1273
+ if (!isPlainRecord2(rawRig)) {
1274
+ return {};
1275
+ }
1276
+ const rawSource = rawRig.taskSource;
1277
+ const metadata = {};
1278
+ if (isPlainRecord2(rawSource)) {
1279
+ const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
1280
+ if (kind.length > 0) {
1281
+ metadata.taskSource = {
1282
+ kind,
1283
+ ...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
1284
+ ...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
1285
+ ...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
1286
+ ...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
1287
+ ...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
1288
+ };
1358
1289
  }
1359
- throw error;
1360
1290
  }
1361
- }
1362
- function runtimeRigGitFileName() {
1363
- return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
1364
- }
1365
- function rigGitSourceCandidates() {
1366
- const execDir = process.execPath?.trim() ? dirname5(process.execPath.trim()) : "";
1367
- const cwd = process.cwd()?.trim() || "";
1368
- const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
1369
- const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
1370
- const moduleRelativeSource = resolve9(import.meta.dir, "../../../native/rig-git.zig");
1371
- return [...new Set([
1372
- process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
1373
- moduleRelativeSource,
1374
- projectRoot ? resolve9(projectRoot, "packages/runtime/native/rig-git.zig") : "",
1375
- hostProjectRoot ? resolve9(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
1376
- cwd ? resolve9(cwd, "packages/runtime/native/rig-git.zig") : "",
1377
- execDir ? resolve9(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
1378
- execDir ? resolve9(execDir, "..", "native", "rig-git.zig") : ""
1379
- ].filter(Boolean))];
1380
- }
1381
- function nativePackageBinaryCandidates(fromDir, fileName) {
1382
- const candidates = [];
1383
- let cursor = resolve9(fromDir);
1384
- for (let index = 0;index < 8; index += 1) {
1385
- candidates.push(resolve9(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve9(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve9(cursor, "native", fileName), resolve9(cursor, "native", "bin", fileName));
1386
- const parent = dirname5(cursor);
1387
- if (parent === cursor)
1388
- break;
1389
- cursor = parent;
1291
+ if (typeof rawRig.sourceIssueId === "string") {
1292
+ metadata.sourceIssueId = rawRig.sourceIssueId;
1390
1293
  }
1391
- return candidates;
1392
- }
1393
- function rigGitBinaryCandidates() {
1394
- const execDir = process.execPath?.trim() ? dirname5(process.execPath.trim()) : "";
1395
- const fileName = runtimeRigGitFileName();
1396
- const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1397
- return [...new Set([
1398
- explicit,
1399
- ...nativePackageBinaryCandidates(import.meta.dir, fileName),
1400
- execDir ? resolve9(execDir, fileName) : "",
1401
- execDir ? resolve9(execDir, "..", fileName) : "",
1402
- execDir ? resolve9(execDir, "..", "bin", fileName) : "",
1403
- sharedGitNativeOutputPath
1404
- ].filter(Boolean))];
1294
+ return metadata;
1405
1295
  }
1406
- function resolveGitSourcePath() {
1407
- for (const candidate of rigGitSourceCandidates()) {
1408
- if (candidate && existsSync7(candidate)) {
1409
- return candidate;
1296
+ function readConfiguredFilesTaskSourcePath(projectRoot) {
1297
+ const jsonPath = resolve9(projectRoot, "rig.config.json");
1298
+ if (existsSync7(jsonPath)) {
1299
+ try {
1300
+ const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
1301
+ if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
1302
+ const source = parsed.taskSource;
1303
+ return source.kind === "files" && typeof source.path === "string" ? source.path : null;
1304
+ }
1305
+ } catch {
1306
+ return null;
1410
1307
  }
1411
1308
  }
1412
- return null;
1413
- }
1414
- function resolveGitBinaryPath() {
1415
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1309
+ const tsPath = resolve9(projectRoot, "rig.config.ts");
1310
+ if (!existsSync7(tsPath)) {
1416
1311
  return null;
1417
1312
  }
1418
- for (const candidate of rigGitBinaryCandidates()) {
1419
- if (candidate && existsSync7(candidate)) {
1420
- return candidate;
1421
- }
1422
- }
1423
- return null;
1424
- }
1425
- function preferredGitBinaryOutputPath() {
1426
- const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1427
- return explicit || sharedGitNativeOutputPath;
1428
- }
1429
- function binarySupportsTrackerCommandsSync(binaryPath) {
1430
1313
  try {
1431
- const probe = Bun.spawnSync([binaryPath, "fetch-ref", "."], {
1432
- stdout: "pipe",
1433
- stderr: "pipe"
1434
- });
1435
- const stdout = probe.stdout.toString().trim();
1436
- const stderr = probe.stderr.toString().trim();
1437
- if (stdout.includes('"error":"unknown command"')) {
1438
- return false;
1314
+ const source = readFileSync6(tsPath, "utf8");
1315
+ const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
1316
+ const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
1317
+ if (kind !== "files") {
1318
+ return null;
1439
1319
  }
1440
- return probe.exitCode === 2 && stderr.includes(trackerCommandUsageProbe);
1320
+ return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
1441
1321
  } catch {
1442
- return false;
1322
+ return null;
1443
1323
  }
1444
1324
  }
1445
- function nativeBuildManifestPath(outputPath) {
1446
- return `${outputPath}.build-manifest.json`;
1447
- }
1448
- function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
1449
- if (!existsSync7(manifestPath)) {
1450
- return false;
1325
+ function readRawTaskEntry(configPath, taskId) {
1326
+ const rawConfig = readRawTaskConfig(configPath);
1327
+ if (!rawConfig) {
1328
+ return null;
1451
1329
  }
1452
- try {
1453
- const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
1454
- return manifest.version === 1 && manifest.buildKey === buildKey;
1455
- } catch {
1456
- return false;
1330
+ const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
1331
+ return isPlainRecord2(entry) ? entry : null;
1332
+ }
1333
+ function readRawTaskConfig(configPath) {
1334
+ if (!existsSync7(configPath)) {
1335
+ return null;
1457
1336
  }
1337
+ const parsed = JSON.parse(readFileSync6(configPath, "utf8"));
1338
+ return isPlainRecord2(parsed) ? parsed : null;
1458
1339
  }
1459
- function sha256FileSync(path) {
1460
- return createHash("sha256").update(readFileSync6(path)).digest("hex");
1340
+ function stripLegacyTaskConfigMetadata2(raw) {
1341
+ const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1342
+ return tasks;
1461
1343
  }
1462
- function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
1463
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1464
- throw new Error("Zig native git is disabled via RIG_DISABLE_ZIG_NATIVE=1");
1465
- }
1466
- const sourcePath = resolveGitSourcePath();
1467
- if (!sourcePath) {
1468
- const binaryPath = resolveGitBinaryPath();
1469
- if (binaryPath) {
1470
- return binaryPath;
1471
- }
1472
- throw new Error("rig-git.zig source file not found.");
1473
- }
1474
- const zigBinary = Bun.which("zig");
1475
- if (!zigBinary) {
1476
- throw new Error("zig is required to build native Rig git tools.");
1477
- }
1478
- mkdirSync4(dirname5(outputPath), { recursive: true });
1479
- const sourceDigest = sha256FileSync(sourcePath);
1480
- const buildKey = JSON.stringify({
1481
- version: 1,
1482
- zigBinary,
1483
- platform: process.platform,
1484
- arch: process.arch,
1485
- sourcePath,
1486
- sourceDigest
1487
- });
1488
- const manifestPath = nativeBuildManifestPath(outputPath);
1489
- const needsBuild = !existsSync7(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
1490
- if (!needsBuild) {
1491
- chmodSync(outputPath, 493);
1492
- return outputPath;
1344
+ function listFileBackedTasks(projectRoot, sourcePath) {
1345
+ const directory = resolve9(projectRoot, sourcePath);
1346
+ if (!existsSync7(directory)) {
1347
+ return [];
1493
1348
  }
1494
- const tempOutputPath = temporaryGitBinaryOutputPath(outputPath);
1495
- const build = Bun.spawnSync([
1496
- zigBinary,
1497
- "build-exe",
1498
- sourcePath,
1499
- "-O",
1500
- "ReleaseFast",
1501
- `-femit-bin=${tempOutputPath}`
1502
- ], {
1503
- cwd: dirname5(sourcePath),
1504
- stdout: "pipe",
1505
- stderr: "pipe"
1506
- });
1507
- if (build.exitCode !== 0 || !existsSync7(tempOutputPath)) {
1508
- const stderr = build.stderr.toString().trim();
1509
- const stdout = build.stdout.toString().trim();
1510
- const details = [stderr, stdout].filter(Boolean).join(`
1511
- `);
1512
- throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
1349
+ const tasks = [];
1350
+ for (const name of readdirSync2(directory)) {
1351
+ if (!FILE_TASK_PATTERN.test(name))
1352
+ continue;
1353
+ const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
1354
+ const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
1355
+ if (task)
1356
+ tasks.push(task);
1513
1357
  }
1514
- chmodSync(tempOutputPath, 493);
1515
- if (existsSync7(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
1516
- rmSync2(tempOutputPath, { force: true });
1517
- chmodSync(outputPath, 493);
1518
- return outputPath;
1358
+ return tasks;
1359
+ }
1360
+ function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
1361
+ const file = findFileBackedTaskFile(resolve9(projectRoot, sourcePath), taskId);
1362
+ if (!file) {
1363
+ return null;
1519
1364
  }
1520
- publishGitBinary(tempOutputPath, outputPath);
1521
- if (!binarySupportsTrackerCommandsSync(outputPath)) {
1522
- rmSync2(outputPath, { force: true });
1523
- throw new Error("Failed to build native Rig git tools: tracker command probe failed");
1365
+ const raw = JSON.parse(readFileSync6(file, "utf8"));
1366
+ if (!isPlainRecord2(raw)) {
1367
+ return null;
1524
1368
  }
1525
- writeFileSync5(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
1526
- `, "utf8");
1527
- return outputPath;
1369
+ return {
1370
+ id: typeof raw.id === "string" ? raw.id : taskId,
1371
+ deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
1372
+ status: typeof raw.status === "string" ? raw.status : "ready",
1373
+ title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
1374
+ ...raw
1375
+ };
1528
1376
  }
1529
- function runGitNative(command, args) {
1530
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1531
- return { ok: false, error: "rig-git native disabled" };
1377
+ function findFileBackedTaskFile(directory, taskId) {
1378
+ if (!existsSync7(directory)) {
1379
+ return null;
1532
1380
  }
1533
- const trackerCommand = command === "fetch-ref" || command === "read-blob-at-ref" || command === "write-tree-commit" || command === "push-ref-with-lease";
1534
- let binaryPath = null;
1535
- if (trackerCommand) {
1381
+ for (const name of readdirSync2(directory)) {
1382
+ if (!FILE_TASK_PATTERN.test(name))
1383
+ continue;
1384
+ const file = join2(directory, name);
1536
1385
  try {
1537
- binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
1538
- } catch (error) {
1539
- const message = error instanceof Error ? error.message : String(error);
1540
- if (message.includes("rig-git.zig source file not found")) {
1541
- return { ok: false, error: "rig-git binary not found" };
1542
- }
1543
- return { ok: false, error: message };
1544
- }
1545
- } else {
1546
- const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1547
- binaryPath = explicitBinaryPath && existsSync7(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
1548
- if (!binaryPath) {
1549
- try {
1550
- binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
1551
- } catch (error) {
1552
- const message = error instanceof Error ? error.message : String(error);
1553
- if (message.includes("rig-git.zig source file not found")) {
1554
- return { ok: false, error: "rig-git binary not found" };
1555
- }
1556
- return { ok: false, error: message };
1386
+ if (!statSync(file).isFile())
1387
+ continue;
1388
+ const raw = JSON.parse(readFileSync6(file, "utf8"));
1389
+ const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
1390
+ const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
1391
+ if (id === taskId) {
1392
+ return file;
1557
1393
  }
1558
- }
1394
+ } catch {}
1559
1395
  }
1560
- try {
1561
- const proc = Bun.spawnSync([binaryPath, command, ...args], {
1562
- stdout: "pipe",
1563
- stderr: "pipe",
1564
- env: process.env
1565
- });
1566
- if (proc.exitCode !== 0) {
1567
- const stdoutText = proc.stdout.toString().trim();
1568
- if (stdoutText) {
1569
- try {
1570
- const parsed = JSON.parse(stdoutText);
1571
- if (!parsed.ok) {
1572
- return parsed;
1573
- }
1574
- } catch {}
1575
- }
1576
- const errText = proc.stderr.toString().trim() || `exit code ${proc.exitCode}`;
1577
- return { ok: false, error: errText };
1396
+ return null;
1397
+ }
1398
+ function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
1399
+ const source = requireGithubIssueSource(metadata, id);
1400
+ const issue = runGh(bin, [
1401
+ "issue",
1402
+ "view",
1403
+ String(id),
1404
+ "--repo",
1405
+ `${source.owner}/${source.repo}`,
1406
+ "--json",
1407
+ "number,title,body,labels,state,url,assignees"
1408
+ ], spawnFn);
1409
+ return githubIssueToTask(issue, source, rawEntry);
1410
+ }
1411
+ function requireGithubIssueSource(metadata, id) {
1412
+ const source = metadata.taskSource;
1413
+ if (source?.kind === "github-issues" && source.owner && source.repo) {
1414
+ return { owner: source.owner, repo: source.repo };
1415
+ }
1416
+ const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
1417
+ if (parsed && parsed[3] === id) {
1418
+ return { owner: parsed[1], repo: parsed[2] };
1419
+ }
1420
+ throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
1421
+ }
1422
+ function githubIssueToTask(issue, source, rawEntry) {
1423
+ const labelNames = (issue.labels ?? []).map((label) => label.name);
1424
+ const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
1425
+ const roleLabel = labelNames.find((label) => label.startsWith("role:"));
1426
+ const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
1427
+ const body = issue.body ?? "";
1428
+ const repo = `${source.owner}/${source.repo}`;
1429
+ return {
1430
+ id: String(issue.number),
1431
+ deps: parseDeps(body),
1432
+ status: githubStatusFor(issue),
1433
+ title: issue.title,
1434
+ body,
1435
+ ...scope.length > 0 ? { scope } : {},
1436
+ ...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
1437
+ ...validators.length > 0 ? { validators } : {},
1438
+ ...issue.url ? { url: issue.url } : {},
1439
+ issueType: issueTypeFor(labelNames),
1440
+ sourceIssueId: `${repo}#${issue.number}`,
1441
+ parentChildDeps: parseParents(body),
1442
+ labels: labelNames,
1443
+ raw: issue,
1444
+ source: "github-issues",
1445
+ _rig: {
1446
+ taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
1447
+ sourceIssueId: `${repo}#${issue.number}`
1578
1448
  }
1579
- const output = proc.stdout.toString().trim();
1580
- return JSON.parse(output);
1581
- } catch (err) {
1582
- return { ok: false, error: String(err) };
1449
+ };
1450
+ }
1451
+ function githubStatusFor(issue) {
1452
+ const state = (issue.state ?? "").toUpperCase();
1453
+ if (state === "CLOSED")
1454
+ return "closed";
1455
+ const labelNames = (issue.labels ?? []).map((label) => label.name);
1456
+ if (labelNames.includes("in-progress"))
1457
+ return "in_progress";
1458
+ if (labelNames.includes("blocked"))
1459
+ return "blocked";
1460
+ if (labelNames.includes("ready"))
1461
+ return "ready";
1462
+ if (labelNames.includes("under-review"))
1463
+ return "under_review";
1464
+ if (labelNames.includes("failed"))
1465
+ return "failed";
1466
+ if (labelNames.includes("cancelled"))
1467
+ return "cancelled";
1468
+ return "open";
1469
+ }
1470
+ function selectedGitHubEnv() {
1471
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1472
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1473
+ }
1474
+ function ghSpawnOptions() {
1475
+ return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
1476
+ }
1477
+ function runGh(bin, args, spawnFn) {
1478
+ const res = spawnFn(bin, [...args], ghSpawnOptions());
1479
+ assertGhSuccess(args, res);
1480
+ if (!res.stdout || res.stdout.trim() === "") {
1481
+ throw new Error(`gh ${args.join(" ")} returned empty stdout`);
1583
1482
  }
1483
+ return JSON.parse(res.stdout);
1584
1484
  }
1585
- function requireGitNative(command, args) {
1586
- const result = runGitNative(command, args);
1587
- if (!result.ok) {
1588
- throw new Error(`rig-git ${command} failed: ${result.error}`);
1485
+ function assertGhSuccess(args, res) {
1486
+ if (res.error) {
1487
+ const msg = res.error.message ?? String(res.error);
1488
+ throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
1489
+ }
1490
+ if (res.status !== 0) {
1491
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
1589
1492
  }
1590
- return result;
1591
1493
  }
1592
- function requireGitNativeString(command, args) {
1593
- const result = requireGitNative(command, args);
1594
- if ("value" in result && typeof result.value === "string") {
1595
- return result.value;
1494
+ function parseDeps(body) {
1495
+ return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
1496
+ }
1497
+ function parseParents(body) {
1498
+ return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
1499
+ }
1500
+ function parseIssueRefs(body, pattern) {
1501
+ const match = body.match(pattern);
1502
+ if (!match)
1503
+ return [];
1504
+ return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
1505
+ }
1506
+ function issueTypeFor(labels) {
1507
+ const typed = labels.find((label) => label.startsWith("type:"));
1508
+ if (typed)
1509
+ return typed.slice("type:".length);
1510
+ if (labels.includes("epic"))
1511
+ return "epic";
1512
+ return "task";
1513
+ }
1514
+ function isPlainRecord2(candidate) {
1515
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1516
+ }
1517
+
1518
+ // packages/runtime/src/control-plane/tasks/source-lifecycle.ts
1519
+ function hasRunnableTaskSource(source) {
1520
+ return Boolean(source && typeof source === "object" && !Array.isArray(source));
1521
+ }
1522
+ async function getPluginTask(projectRoot, taskId) {
1523
+ const ctx = await buildPluginHostContext(projectRoot);
1524
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
1525
+ if (!hasRunnableTaskSource(source)) {
1526
+ return ctx ? { configured: false, sourceKind: null, task: null } : null;
1596
1527
  }
1597
- throw new Error(`rig-git ${command} returned an unexpected result payload`);
1528
+ const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
1529
+ return {
1530
+ configured: true,
1531
+ sourceKind: source.kind,
1532
+ task
1533
+ };
1598
1534
  }
1599
- function nativeFetchRef(repoPath, remote, branch) {
1600
- return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
1535
+ async function readConfiguredTaskSourceTask(projectRoot, taskId) {
1536
+ const pluginResult = await getPluginTask(projectRoot, taskId);
1537
+ if (pluginResult)
1538
+ return pluginResult;
1539
+ const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
1540
+ return {
1541
+ configured: false,
1542
+ sourceKind: null,
1543
+ task
1544
+ };
1601
1545
  }
1602
- function nativeReadBlobAtRef(repoPath, ref, path) {
1603
- const requestDir = resolve9(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
1604
- mkdirSync4(requestDir, { recursive: true });
1605
- const outputPath = resolve9(requestDir, "blob.txt");
1606
- try {
1607
- requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
1608
- return readFileSync6(outputPath, "utf8");
1609
- } finally {
1610
- rmSync2(requestDir, { recursive: true, force: true });
1546
+
1547
+ // packages/runtime/src/control-plane/native/task-state.ts
1548
+ import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
1549
+ import { basename as basename6, resolve as resolve16 } from "path";
1550
+
1551
+ // packages/runtime/src/control-plane/state-sync/types.ts
1552
+ var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
1553
+ var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
1554
+ "draft",
1555
+ "open",
1556
+ "ready",
1557
+ "queued",
1558
+ "in_progress",
1559
+ "under_review",
1560
+ "blocked",
1561
+ "completed",
1562
+ "cancelled"
1563
+ ]);
1564
+ function normalizeTaskLifecycleStatus(status) {
1565
+ switch (status) {
1566
+ case "draft":
1567
+ case "open":
1568
+ case "ready":
1569
+ case "queued":
1570
+ case "in_progress":
1571
+ case "under_review":
1572
+ case "blocked":
1573
+ case "completed":
1574
+ case "cancelled":
1575
+ return status;
1576
+ case "closed":
1577
+ return "completed";
1578
+ case "running":
1579
+ return "in_progress";
1580
+ case "failed":
1581
+ return "ready";
1582
+ default:
1583
+ return null;
1611
1584
  }
1612
1585
  }
1613
- function serializeTreeCommitUpdates(updates) {
1614
- return updates.map((update) => {
1615
- if (isTextTreeCommitUpdate(update)) {
1616
- return { path: update.path, kind: "text", content: update.content };
1617
- }
1618
- if (!isAbsolute(update.sourceFilePath)) {
1619
- throw new Error("tree commit binary updates require an absolute sourceFilePath");
1620
- }
1621
- return { path: update.path, kind: "file", sourceFilePath: update.sourceFilePath };
1622
- });
1586
+ function normalizeTaskStateMetadataStatus(status) {
1587
+ if (CANONICAL_TASK_LIFECYCLE_STATUSES.has(status)) {
1588
+ return status;
1589
+ }
1590
+ switch (status) {
1591
+ case "closed":
1592
+ return "completed";
1593
+ case "running":
1594
+ return "in_progress";
1595
+ case "failed":
1596
+ return "ready";
1597
+ default:
1598
+ return;
1599
+ }
1623
1600
  }
1624
- function buildTreeCommitUpdatesJson(updates) {
1625
- return `${JSON.stringify(serializeTreeCommitUpdates(updates), null, 2)}
1626
- `;
1601
+ function canonicalizeTaskStateMetadata(raw) {
1602
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1603
+ return null;
1604
+ }
1605
+ const metadata = raw;
1606
+ const claimId = typeof metadata.claimId === "string" && metadata.claimId.length > 0 ? metadata.claimId : undefined;
1607
+ const status = normalizeTaskStateMetadataStatus(metadata.status);
1608
+ if (!status) {
1609
+ return null;
1610
+ }
1611
+ return {
1612
+ ...claimId ? { claimId } : {},
1613
+ status,
1614
+ ...typeof metadata.ownerId === "string" && metadata.ownerId.length > 0 ? { ownerId: metadata.ownerId } : {},
1615
+ ...typeof metadata.claimedAt === "string" && metadata.claimedAt.length > 0 ? { claimedAt: metadata.claimedAt } : {},
1616
+ ...typeof metadata.lastEvidenceAt === "string" && metadata.lastEvidenceAt.length > 0 ? { lastEvidenceAt: metadata.lastEvidenceAt } : {},
1617
+ ...typeof metadata.runId === "string" && metadata.runId.length > 0 ? { runId: metadata.runId } : {},
1618
+ ...typeof metadata.branchName === "string" && metadata.branchName.length > 0 ? { branchName: metadata.branchName } : {},
1619
+ ...typeof metadata.prNumber === "number" ? { prNumber: metadata.prNumber } : {},
1620
+ ...typeof metadata.prUrl === "string" && metadata.prUrl.length > 0 ? { prUrl: metadata.prUrl } : {},
1621
+ ...typeof metadata.reviewState === "string" && metadata.reviewState.length > 0 ? { reviewState: metadata.reviewState } : {},
1622
+ ...typeof metadata.blockerReason === "string" && metadata.blockerReason.length > 0 ? { blockerReason: metadata.blockerReason } : {},
1623
+ ...typeof metadata.sourceCommit === "string" && metadata.sourceCommit.length > 0 ? { sourceCommit: metadata.sourceCommit } : {}
1624
+ };
1627
1625
  }
1628
- function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
1629
- const requestDir = resolve9(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
1630
- mkdirSync4(requestDir, { recursive: true });
1631
- const messagePath = resolve9(requestDir, "message.txt");
1632
- const updatesPath = resolve9(requestDir, "updates.json");
1633
- try {
1634
- writeFileSync5(messagePath, message, "utf8");
1635
- writeFileSync5(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
1636
- return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
1637
- } finally {
1638
- rmSync2(requestDir, { recursive: true, force: true });
1626
+ function discardMismatchedTaskStateMetadata(input) {
1627
+ input.taskId;
1628
+ const canonicalMetadata = canonicalizeTaskStateMetadata(input.metadata);
1629
+ if (!canonicalMetadata || !input.lifecycleStatus) {
1630
+ return null;
1631
+ }
1632
+ const metadataStatus = canonicalMetadata.status ?? null;
1633
+ if (metadataStatus && metadataStatus !== input.lifecycleStatus) {
1634
+ return null;
1639
1635
  }
1636
+ return canonicalMetadata;
1640
1637
  }
1641
- function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
1642
- return requireGitNativeString("push-ref-with-lease", [
1643
- repoPath,
1644
- localOid,
1645
- remoteRef,
1646
- expectedOldOid,
1647
- remote
1648
- ]);
1638
+ function readTaskStateMetadataEnvelope(raw) {
1639
+ if (!raw || typeof raw !== "object") {
1640
+ return { schemaVersion: SUPPORTED_TASK_STATE_SCHEMA_VERSION, supported: true, baseTrackerCommit: null, tasks: {} };
1641
+ }
1642
+ const envelope = raw;
1643
+ const schemaVersion = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : SUPPORTED_TASK_STATE_SCHEMA_VERSION;
1644
+ if (schemaVersion !== SUPPORTED_TASK_STATE_SCHEMA_VERSION) {
1645
+ return { schemaVersion, supported: false, baseTrackerCommit: null, tasks: {} };
1646
+ }
1647
+ const rawTasks = envelope.tasks && typeof envelope.tasks === "object" && !Array.isArray(envelope.tasks) ? envelope.tasks : {};
1648
+ const tasks = Object.fromEntries(Object.entries(rawTasks).map(([taskId, metadata]) => [taskId, canonicalizeTaskStateMetadata(metadata)]).filter((entry) => entry[1] != null));
1649
+ return {
1650
+ schemaVersion,
1651
+ supported: true,
1652
+ baseTrackerCommit: typeof envelope.baseTrackerCommit === "string" && envelope.baseTrackerCommit.length > 0 ? envelope.baseTrackerCommit : null,
1653
+ tasks
1654
+ };
1649
1655
  }
1656
+ // packages/runtime/src/control-plane/state-sync/read.ts
1657
+ import { existsSync as existsSync13, readFileSync as readFileSync8 } from "fs";
1658
+ import { resolve as resolve15 } from "path";
1650
1659
 
1651
1660
  // packages/runtime/src/control-plane/native/utils.ts
1652
- import { ptr as ptr2 } from "bun:ffi";
1653
1661
  import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
1654
1662
  import { resolve as resolve12 } from "path";
1655
1663
 
@@ -1771,12 +1779,6 @@ var sharedNativeRuntimeOutputDir = resolve11(tmpdir4(), "rig-native");
1771
1779
  var sharedNativeRuntimeOutputPath = resolve11(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
1772
1780
  var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
1773
1781
  var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
1774
- function requireNativeRuntimeLibrary(feature) {
1775
- if (!nativeRuntimeLibrary) {
1776
- throw new Error(`Native Zig runtime is required for ${feature}`);
1777
- }
1778
- return nativeRuntimeLibrary;
1779
- }
1780
1782
  async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOutputPath, options = {}) {
1781
1783
  if (await buildNativeRuntimeLibrary(outputPath, options)) {
1782
1784
  return outputPath;
@@ -1942,7 +1944,6 @@ function tryDlopenNativeRuntimeLibrary(outputPath) {
1942
1944
  function resolveMonorepoRoot2(projectRoot) {
1943
1945
  return resolveMonorepoRoot(projectRoot);
1944
1946
  }
1945
- var nativeScopeMatcher = null;
1946
1947
  var scopeRegexCache = new Map;
1947
1948
  function runCapture(command, cwd, env) {
1948
1949
  const result = Bun.spawnSync(command, {
@@ -2099,41 +2100,6 @@ function monorepoSearchCandidates(inputPath) {
2099
2100
  }
2100
2101
  return [...candidates];
2101
2102
  }
2102
- function scopeMatches(path, scopes) {
2103
- const matcher = getNativeScopeMatcher();
2104
- const pathVariants = unique([path, normalizeRelativeScopePath(path)]);
2105
- for (const scope of scopes) {
2106
- const scopeVariants = unique([scope, normalizeRelativeScopePath(scope)]);
2107
- for (const candidatePath of pathVariants) {
2108
- for (const candidateScope of scopeVariants) {
2109
- if (candidatePath === candidateScope) {
2110
- return true;
2111
- }
2112
- if (matcher.match(candidateScope, candidatePath)) {
2113
- return true;
2114
- }
2115
- }
2116
- }
2117
- }
2118
- return false;
2119
- }
2120
- function getNativeScopeMatcher() {
2121
- if (nativeScopeMatcher) {
2122
- return nativeScopeMatcher;
2123
- }
2124
- nativeScopeMatcher = createNativeScopeMatcher();
2125
- return nativeScopeMatcher;
2126
- }
2127
- function createNativeScopeMatcher() {
2128
- const library = requireNativeRuntimeLibrary("scope matching");
2129
- return {
2130
- match: (pattern, path) => {
2131
- const patternBuffer = Buffer.from(`${pattern}\x00`);
2132
- const pathBuffer = Buffer.from(`${path}\x00`);
2133
- return library.symbols.rig_scope_match(Number(ptr2(patternBuffer)), Number(ptr2(pathBuffer))) !== 0;
2134
- }
2135
- };
2136
- }
2137
2103
 
2138
2104
  // packages/runtime/src/control-plane/state-sync/repo.ts
2139
2105
  import { existsSync as existsSync12 } from "fs";
@@ -7209,20 +7175,7 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
7209
7175
  if (!taskId) {
7210
7176
  return unique(uniqueFiles).sort();
7211
7177
  }
7212
- const filteredFiles = unique(uniqueFiles).filter((file) => !isGeneratedTaskChangePath(taskId, file));
7213
- if (!scoped) {
7214
- return filteredFiles.sort();
7215
- }
7216
- const paths = resolveHarnessPaths(projectRoot);
7217
- const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
7218
- const scopes = readTaskConfigForInvocation(projectRoot)[taskId]?.scope || [];
7219
- if (scopes.length === 0) {
7220
- return [];
7221
- }
7222
- return filteredFiles.filter((file) => {
7223
- const normalized = normalizePathToScope(projectRoot, monorepoRoot || paths.monorepoRoot, file);
7224
- return scopeMatches(file, scopes) || scopeMatches(normalized, scopes);
7225
- }).sort();
7178
+ return unique(uniqueFiles).filter((file) => !isGeneratedTaskChangePath(taskId, file)).sort();
7226
7179
  }
7227
7180
  function resolveTaskMonorepoRoot(projectRoot) {
7228
7181
  const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
@@ -7273,6 +7226,17 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
7273
7226
  }
7274
7227
  function collectWorkingTreeFiles(projectRoot, repo, baseline) {
7275
7228
  const files = new Set;
7229
+ const nativeFiles = nativePendingFiles(repo);
7230
+ if (nativeFiles !== null) {
7231
+ for (const entry of nativeFiles) {
7232
+ const normalized = normalizeChangedFilePath(entry.path);
7233
+ if (!normalized || baseline.has(normalized)) {
7234
+ continue;
7235
+ }
7236
+ files.add(normalized);
7237
+ }
7238
+ return [...files].sort();
7239
+ }
7276
7240
  for (const args of [
7277
7241
  ["diff", "--name-only"],
7278
7242
  ["diff", "--cached", "--name-only"],