@h-rig/runtime 0.0.6-alpha.31 → 0.0.6-alpha.33

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.
@@ -299,1518 +299,1517 @@ function readBuildConfig() {
299
299
  }
300
300
  }
301
301
 
302
- // packages/runtime/src/control-plane/runtime/tooling/shell.ts
302
+ // packages/runtime/src/control-plane/native/git-native.ts
303
+ import { chmodSync, copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
303
304
  import { tmpdir } from "os";
304
- import { basename, dirname as dirname2, resolve as resolve3 } from "path";
305
- var sharedNativeShellOutputDir = resolve3(tmpdir(), "rig-native");
306
- var sharedNativeShellOutputPath = resolve3(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
307
- function runtimeToolGatewayNames() {
308
- return [
309
- "bash",
310
- "sh",
311
- "zsh",
312
- "git",
313
- "bun",
314
- "node",
315
- "python3",
316
- "rg",
317
- "grep",
318
- "sed",
319
- "cat",
320
- "ls",
321
- "find",
322
- "tsc",
323
- "gh",
324
- "mkdir",
325
- "rm",
326
- "mv",
327
- "cp",
328
- "touch",
329
- "pwd",
330
- "head",
331
- "tail",
332
- "wc",
333
- "sort",
334
- "uniq",
335
- "awk",
336
- "xargs",
337
- "dirname",
338
- "basename",
339
- "realpath",
340
- "env",
341
- "jq",
342
- "tee",
343
- "which"
344
- ];
345
- }
346
- // packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
347
- import { tmpdir as tmpdir2 } from "os";
348
- import { basename as basename2, dirname as dirname3, resolve as resolve4 } from "path";
349
- var sharedNativeToolsOutputDir = resolve4(tmpdir2(), "rig-native");
350
- var sharedNativeToolsOutputPath = resolve4(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
351
- function runtimeFileToolNames() {
352
- return [
353
- "rig-read",
354
- "rig-write",
355
- "rig-edit",
356
- "rig-glob",
357
- "rig-grep"
358
- ];
359
- }
360
-
361
- // packages/runtime/src/control-plane/runtime/tooling/gateway.ts
362
- function runtimeGatewayToolNames() {
363
- return runtimeToolGatewayNames();
364
- }
365
- // packages/runtime/src/control-plane/browser-contract.ts
366
- import { resolve as resolve5 } from "path";
367
- var DEFAULT_BROWSER_ATTACH_URL = "http://127.0.0.1:9333";
368
- var DEFAULT_BROWSER_MODE = "persistent";
369
- var RUNTIME_BROWSER_HELPERS = {
370
- launch: "rig-browser-launch",
371
- check: "rig-browser-check",
372
- attachInfo: "rig-browser-attach-info",
373
- e2e: "rig-browser-e2e",
374
- resetProfile: "rig-browser-reset-profile"
375
- };
376
- var BASE_REMOTE_DEBUGGING_PORT = 9222;
377
- var REMOTE_DEBUGGING_PORT_SPREAD = 4000;
378
- function hashString(input) {
379
- let hash = 0;
380
- for (let index = 0;index < input.length; index += 1) {
381
- hash = (hash << 5) - hash + input.charCodeAt(index) | 0;
382
- }
383
- return Math.abs(hash);
305
+ import { dirname as dirname2, isAbsolute, resolve as resolve3 } from "path";
306
+ import { createHash } from "crypto";
307
+ function isTextTreeCommitUpdate(update) {
308
+ return typeof update.content === "string";
384
309
  }
385
- function derivePortFromProfile(profileName) {
386
- return BASE_REMOTE_DEBUGGING_PORT + hashString(profileName) % REMOTE_DEBUGGING_PORT_SPREAD;
310
+ var sharedGitNativeOutputDir = resolve3(tmpdir(), "rig-native");
311
+ var sharedGitNativeOutputPath = resolve3(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
312
+ var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
313
+ function temporaryGitBinaryOutputPath(outputPath) {
314
+ const suffix = process.platform === "win32" ? ".exe" : "";
315
+ return resolve3(dirname2(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
387
316
  }
388
- function parseAttachUrl(attachUrl) {
317
+ function publishGitBinary(tempOutputPath, outputPath) {
389
318
  try {
390
- return new URL((attachUrl || DEFAULT_BROWSER_ATTACH_URL).trim() || DEFAULT_BROWSER_ATTACH_URL);
391
- } catch {
392
- return new URL(DEFAULT_BROWSER_ATTACH_URL);
319
+ renameSync(tempOutputPath, outputPath);
320
+ } catch (error) {
321
+ if (process.platform === "win32" && existsSync3(outputPath)) {
322
+ rmSync(outputPath, { force: true });
323
+ renameSync(tempOutputPath, outputPath);
324
+ return;
325
+ }
326
+ throw error;
393
327
  }
394
328
  }
395
- function sanitizeRuntimeSuffix(runtimeId) {
396
- return runtimeId.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 16) || "runtime";
329
+ function runtimeRigGitFileName() {
330
+ return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
397
331
  }
398
- function resolveBrowserStateDir(projectRoot, configuredStateDir) {
399
- const trimmed = configuredStateDir?.trim() || ".tmp/rig-browser";
400
- if (trimmed.startsWith("/")) {
401
- return resolve5(trimmed);
332
+ function rigGitSourceCandidates() {
333
+ const execDir = process.execPath?.trim() ? dirname2(process.execPath.trim()) : "";
334
+ const cwd = process.cwd()?.trim() || "";
335
+ const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
336
+ const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
337
+ const moduleRelativeSource = resolve3(import.meta.dir, "../../../native/rig-git.zig");
338
+ return [...new Set([
339
+ process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
340
+ moduleRelativeSource,
341
+ projectRoot ? resolve3(projectRoot, "packages/runtime/native/rig-git.zig") : "",
342
+ hostProjectRoot ? resolve3(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
343
+ cwd ? resolve3(cwd, "packages/runtime/native/rig-git.zig") : "",
344
+ execDir ? resolve3(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
345
+ execDir ? resolve3(execDir, "..", "native", "rig-git.zig") : ""
346
+ ].filter(Boolean))];
347
+ }
348
+ function nativePackageBinaryCandidates(fromDir, fileName) {
349
+ const candidates = [];
350
+ let cursor = resolve3(fromDir);
351
+ for (let index = 0;index < 8; index += 1) {
352
+ candidates.push(resolve3(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve3(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve3(cursor, "native", fileName), resolve3(cursor, "native", "bin", fileName));
353
+ const parent = dirname2(cursor);
354
+ if (parent === cursor)
355
+ break;
356
+ cursor = parent;
402
357
  }
403
- return resolve5(projectRoot || process.cwd(), trimmed);
358
+ return candidates;
404
359
  }
405
- function resolveTaskBrowserContext(browser, options = {}) {
406
- if (!browser?.required) {
407
- return;
360
+ function rigGitBinaryCandidates() {
361
+ const execDir = process.execPath?.trim() ? dirname2(process.execPath.trim()) : "";
362
+ const fileName = runtimeRigGitFileName();
363
+ const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
364
+ return [...new Set([
365
+ explicit,
366
+ ...nativePackageBinaryCandidates(import.meta.dir, fileName),
367
+ execDir ? resolve3(execDir, fileName) : "",
368
+ execDir ? resolve3(execDir, "..", fileName) : "",
369
+ execDir ? resolve3(execDir, "..", "bin", fileName) : "",
370
+ sharedGitNativeOutputPath
371
+ ].filter(Boolean))];
372
+ }
373
+ function resolveGitSourcePath() {
374
+ for (const candidate of rigGitSourceCandidates()) {
375
+ if (candidate && existsSync3(candidate)) {
376
+ return candidate;
377
+ }
408
378
  }
409
- const defaultProfile = browser.profile?.trim() || "default";
410
- const mode = browser.mode?.trim() || DEFAULT_BROWSER_MODE;
411
- const defaultAttach = parseAttachUrl(browser.attach_url);
412
- const shouldDeriveRuntimeProfile = Boolean(options.runtimeId?.trim()) && mode !== "shared";
413
- const effectiveProfile = shouldDeriveRuntimeProfile ? `${defaultProfile}-${sanitizeRuntimeSuffix(options.runtimeId.trim())}` : defaultProfile;
414
- const effectivePort = shouldDeriveRuntimeProfile ? derivePortFromProfile(effectiveProfile) : Number(defaultAttach.port || "80");
415
- const effectiveAttach = new URL(defaultAttach.toString());
416
- effectiveAttach.port = String(effectivePort);
417
- return {
418
- required: true,
419
- preset: browser.preset?.trim() || "default",
420
- mode,
421
- stateDir: resolveBrowserStateDir(options.hostProjectRoot, browser.state_dir),
422
- defaultProfile,
423
- effectiveProfile,
424
- defaultAttachUrl: defaultAttach.toString(),
425
- effectiveAttachUrl: effectiveAttach.toString(),
426
- devCommand: browser.dev_command?.trim() || undefined,
427
- launchCommand: browser.launch_command?.trim() || undefined,
428
- checkCommand: browser.check_command?.trim() || undefined,
429
- e2eCommand: browser.e2e_command?.trim() || undefined,
430
- launchHelper: RUNTIME_BROWSER_HELPERS.launch,
431
- checkHelper: RUNTIME_BROWSER_HELPERS.check,
432
- attachInfoHelper: RUNTIME_BROWSER_HELPERS.attachInfo,
433
- e2eHelper: RUNTIME_BROWSER_HELPERS.e2e,
434
- resetProfileHelper: RUNTIME_BROWSER_HELPERS.resetProfile
435
- };
379
+ return null;
436
380
  }
437
- function buildBrowserGuidanceLines(browser) {
438
- const lines = [
439
- "This task requires Rig Browser.",
440
- `Launch the browser: \`${browser.launchHelper}\`${browser.devCommand ? " or `rig-browser-launch --dev`" : ""}.`,
441
- `Check the browser contract: \`${browser.checkHelper}\`.`,
442
- `Show attach details: \`${browser.attachInfoHelper}\`.`,
443
- `Attach Chrome DevTools MCP to ${browser.effectiveAttachUrl}.`,
444
- `Preset: ${browser.preset}.`,
445
- `Profile: ${browser.effectiveProfile}.`,
446
- `State dir: ${browser.stateDir}.`,
447
- `Reset the active profile with \`${browser.resetProfileHelper}\`.`
448
- ];
449
- if (browser.e2eCommand) {
450
- lines.push(`Run app-owned browser e2e with \`${browser.e2eHelper}\`.`);
381
+ function resolveGitBinaryPath() {
382
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
383
+ return null;
451
384
  }
452
- if (browser.defaultProfile !== browser.effectiveProfile) {
453
- lines.push(`Base profile: ${browser.defaultProfile}. Runtime launches derive an isolated effective profile.`);
385
+ for (const candidate of rigGitBinaryCandidates()) {
386
+ if (candidate && existsSync3(candidate)) {
387
+ return candidate;
388
+ }
454
389
  }
455
- return lines;
390
+ return null;
456
391
  }
457
-
458
- // packages/runtime/src/control-plane/plugin-host-context.ts
459
- import { createPluginHost } from "@rig/core";
460
- import { loadConfig } from "@rig/core/load-config";
461
-
462
- // packages/runtime/src/control-plane/task-source.ts
463
- function createTaskSourceRegistry() {
464
- const byId = new Map;
465
- const order = [];
466
- return {
467
- register(s) {
468
- if (byId.has(s.id))
469
- throw new Error(`task source already registered: ${s.id}`);
470
- byId.set(s.id, s);
471
- order.push(s);
472
- },
473
- resolveById(id) {
474
- const s = byId.get(id);
475
- if (!s)
476
- throw new Error(`task source not registered: ${id}`);
477
- return s;
478
- },
479
- resolveByKind(kind) {
480
- for (const s of order)
481
- if (s.kind === kind)
482
- return s;
483
- throw new Error(`no task source registered for kind: ${kind}`);
484
- },
485
- list: () => order
486
- };
392
+ function preferredGitBinaryOutputPath() {
393
+ const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
394
+ return explicit || sharedGitNativeOutputPath;
487
395
  }
488
-
489
- // packages/runtime/src/control-plane/task-source-bootstrap.ts
490
- function formatRegisteredKinds(pluginHost) {
491
- const kinds = pluginHost ? pluginHost.listExecutableTaskSources().map((source) => source.kind) : [];
492
- return kinds.length > 0 ? kinds.join(", ") : "none";
396
+ function binarySupportsTrackerCommandsSync(binaryPath) {
397
+ try {
398
+ const probe = Bun.spawnSync([binaryPath, "fetch-ref", "."], {
399
+ stdout: "pipe",
400
+ stderr: "pipe"
401
+ });
402
+ const stdout = probe.stdout.toString().trim();
403
+ const stderr = probe.stderr.toString().trim();
404
+ if (stdout.includes('"error":"unknown command"')) {
405
+ return false;
406
+ }
407
+ return probe.exitCode === 2 && stderr.includes(trackerCommandUsageProbe);
408
+ } catch {
409
+ return false;
410
+ }
493
411
  }
494
- function buildTaskSourceRegistry(config, pluginHost) {
495
- const registry = createTaskSourceRegistry();
496
- const taskSourceConfig = config.taskSource;
497
- const factory = pluginHost?.resolveTaskSourceFactoryByKind(taskSourceConfig.kind);
498
- if (!factory) {
499
- 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.");
412
+ function nativeBuildManifestPath(outputPath) {
413
+ return `${outputPath}.build-manifest.json`;
414
+ }
415
+ function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
416
+ if (!existsSync3(manifestPath)) {
417
+ return false;
418
+ }
419
+ try {
420
+ const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
421
+ return manifest.version === 1 && manifest.buildKey === buildKey;
422
+ } catch {
423
+ return false;
500
424
  }
501
- registry.register(factory.factory(taskSourceConfig));
502
- return registry;
503
425
  }
504
-
505
- // packages/runtime/src/control-plane/repos/registry.ts
506
- function createRepoRegistry(entries) {
507
- const map = new Map;
508
- for (const e of entries) {
509
- if (map.has(e.id))
510
- throw new Error(`repo already registered: ${e.id}`);
511
- map.set(e.id, { ...e });
426
+ function sha256FileSync(path) {
427
+ return createHash("sha256").update(readFileSync3(path)).digest("hex");
428
+ }
429
+ function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
430
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
431
+ throw new Error("Zig native git is disabled via RIG_DISABLE_ZIG_NATIVE=1");
512
432
  }
513
- const ordered = Array.from(map.values());
514
- return {
515
- getById: (id) => map.get(id),
516
- list: () => ordered
517
- };
433
+ const sourcePath = resolveGitSourcePath();
434
+ if (!sourcePath) {
435
+ const binaryPath = resolveGitBinaryPath();
436
+ if (binaryPath) {
437
+ return binaryPath;
438
+ }
439
+ throw new Error("rig-git.zig source file not found.");
440
+ }
441
+ const zigBinary = Bun.which("zig");
442
+ if (!zigBinary) {
443
+ throw new Error("zig is required to build native Rig git tools.");
444
+ }
445
+ mkdirSync2(dirname2(outputPath), { recursive: true });
446
+ const sourceDigest = sha256FileSync(sourcePath);
447
+ const buildKey = JSON.stringify({
448
+ version: 1,
449
+ zigBinary,
450
+ platform: process.platform,
451
+ arch: process.arch,
452
+ sourcePath,
453
+ sourceDigest
454
+ });
455
+ const manifestPath = nativeBuildManifestPath(outputPath);
456
+ const needsBuild = !existsSync3(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
457
+ if (!needsBuild) {
458
+ chmodSync(outputPath, 493);
459
+ return outputPath;
460
+ }
461
+ const tempOutputPath = temporaryGitBinaryOutputPath(outputPath);
462
+ const build = Bun.spawnSync([
463
+ zigBinary,
464
+ "build-exe",
465
+ sourcePath,
466
+ "-O",
467
+ "ReleaseFast",
468
+ `-femit-bin=${tempOutputPath}`
469
+ ], {
470
+ cwd: dirname2(sourcePath),
471
+ stdout: "pipe",
472
+ stderr: "pipe"
473
+ });
474
+ if (build.exitCode !== 0 || !existsSync3(tempOutputPath)) {
475
+ const stderr = build.stderr.toString().trim();
476
+ const stdout = build.stdout.toString().trim();
477
+ const details = [stderr, stdout].filter(Boolean).join(`
478
+ `);
479
+ throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
480
+ }
481
+ chmodSync(tempOutputPath, 493);
482
+ if (existsSync3(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
483
+ rmSync(tempOutputPath, { force: true });
484
+ chmodSync(outputPath, 493);
485
+ return outputPath;
486
+ }
487
+ publishGitBinary(tempOutputPath, outputPath);
488
+ if (!binarySupportsTrackerCommandsSync(outputPath)) {
489
+ rmSync(outputPath, { force: true });
490
+ throw new Error("Failed to build native Rig git tools: tracker command probe failed");
491
+ }
492
+ writeFileSync2(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
493
+ `, "utf8");
494
+ return outputPath;
518
495
  }
519
- var MANAGED_REPOS = new Map;
520
- function setManagedRepos(entries) {
521
- const next = new Map;
522
- for (const e of entries) {
523
- if (next.has(e.id)) {
524
- throw new Error(`managed repo already registered: ${e.id}`);
496
+ function runGitNative(command, args) {
497
+ if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
498
+ return { ok: false, error: "rig-git native disabled" };
499
+ }
500
+ const trackerCommand = command === "fetch-ref" || command === "read-blob-at-ref" || command === "write-tree-commit" || command === "push-ref-with-lease";
501
+ let binaryPath = null;
502
+ if (trackerCommand) {
503
+ try {
504
+ binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
505
+ } catch (error) {
506
+ const message = error instanceof Error ? error.message : String(error);
507
+ if (message.includes("rig-git.zig source file not found")) {
508
+ return { ok: false, error: "rig-git binary not found" };
509
+ }
510
+ return { ok: false, error: message };
511
+ }
512
+ } else {
513
+ const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
514
+ binaryPath = explicitBinaryPath && existsSync3(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
515
+ if (!binaryPath) {
516
+ try {
517
+ binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
518
+ } catch (error) {
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ if (message.includes("rig-git.zig source file not found")) {
521
+ return { ok: false, error: "rig-git binary not found" };
522
+ }
523
+ return { ok: false, error: message };
524
+ }
525
525
  }
526
- next.set(e.id, e);
527
526
  }
528
- MANAGED_REPOS = next;
527
+ try {
528
+ const proc = Bun.spawnSync([binaryPath, command, ...args], {
529
+ stdout: "pipe",
530
+ stderr: "pipe",
531
+ env: process.env
532
+ });
533
+ if (proc.exitCode !== 0) {
534
+ const stdoutText = proc.stdout.toString().trim();
535
+ if (stdoutText) {
536
+ try {
537
+ const parsed = JSON.parse(stdoutText);
538
+ if (!parsed.ok) {
539
+ return parsed;
540
+ }
541
+ } catch {}
542
+ }
543
+ const errText = proc.stderr.toString().trim() || `exit code ${proc.exitCode}`;
544
+ return { ok: false, error: errText };
545
+ }
546
+ const output = proc.stdout.toString().trim();
547
+ return JSON.parse(output);
548
+ } catch (err) {
549
+ return { ok: false, error: String(err) };
550
+ }
529
551
  }
530
- function getManagedRepoEntry(repoId) {
531
- const entry = MANAGED_REPOS.get(repoId);
532
- if (!entry) {
533
- 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.`);
552
+ function requireGitNative(command, args) {
553
+ const result = runGitNative(command, args);
554
+ if (!result.ok) {
555
+ throw new Error(`rig-git ${command} failed: ${result.error}`);
534
556
  }
535
- return entry;
557
+ return result;
536
558
  }
537
- function listManagedRepoEntries() {
538
- return Array.from(MANAGED_REPOS.values());
559
+ function requireGitNativeString(command, args) {
560
+ const result = requireGitNative(command, args);
561
+ if ("value" in result && typeof result.value === "string") {
562
+ return result.value;
563
+ }
564
+ throw new Error(`rig-git ${command} returned an unexpected result payload`);
539
565
  }
540
- function resolveManagedRepoIdByAlias(alias) {
541
- for (const entry of MANAGED_REPOS.values()) {
542
- if (entry.alias === alias) {
543
- return entry.id;
544
- }
566
+ function nativeBranchName(repoPath) {
567
+ const result = runGitNative("branch-name", [repoPath]);
568
+ if (!result.ok)
569
+ return null;
570
+ if ("value" in result && typeof result.value === "string")
571
+ return result.value;
572
+ return null;
573
+ }
574
+ function nativeHeadOid(repoPath) {
575
+ const result = runGitNative("head-oid", [repoPath]);
576
+ if (!result.ok)
577
+ return null;
578
+ if ("value" in result && typeof result.value === "string")
579
+ return result.value;
580
+ return null;
581
+ }
582
+ function nativeChangeCount(repoPath) {
583
+ const result = runGitNative("change-count", [repoPath]);
584
+ if (!result.ok)
585
+ return null;
586
+ if ("count" in result && typeof result.count === "number")
587
+ return result.count;
588
+ return null;
589
+ }
590
+ function nativePendingFiles(repoPath) {
591
+ const result = runGitNative("pending-files", [repoPath]);
592
+ if (!result.ok)
593
+ return null;
594
+ if ("files" in result && Array.isArray(result.files)) {
595
+ return result.files.map((f) => ({ path: f.path, status: f.status }));
545
596
  }
546
597
  return null;
547
598
  }
548
- function repoRegistrationToManagedEntry(reg) {
549
- if (!reg.defaultBranch) {
599
+ function nativeFileHasChanges(repoPath, filePath) {
600
+ const result = runGitNative("file-has-changes", [repoPath, filePath]);
601
+ if (!result.ok)
550
602
  return null;
603
+ if ("has_changes" in result && typeof result.has_changes === "boolean")
604
+ return result.has_changes;
605
+ return null;
606
+ }
607
+ function nativeFetchRef(repoPath, remote, branch) {
608
+ return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
609
+ }
610
+ function nativeReadBlobAtRef(repoPath, ref, path) {
611
+ const requestDir = resolve3(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
612
+ mkdirSync2(requestDir, { recursive: true });
613
+ const outputPath = resolve3(requestDir, "blob.txt");
614
+ try {
615
+ requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
616
+ return readFileSync3(outputPath, "utf8");
617
+ } finally {
618
+ rmSync(requestDir, { recursive: true, force: true });
551
619
  }
552
- return {
553
- id: reg.id,
554
- alias: reg.defaultPath ?? reg.id,
555
- defaultBranch: reg.defaultBranch,
556
- defaultRemoteUrl: reg.url,
557
- remoteEnvVar: reg.remoteEnvVar,
558
- checkoutEnvVar: reg.checkoutEnvVar
559
- };
560
620
  }
561
-
562
- // packages/runtime/src/control-plane/agent-roles.ts
563
- function createAgentRoleRegistry(pluginRoles, configOverrides) {
564
- const map = new Map;
565
- for (const r of pluginRoles) {
566
- if (map.has(r.id))
567
- throw new Error(`agent role already registered: ${r.id}`);
568
- const override = configOverrides?.[r.id];
569
- const model = override?.model ?? r.defaultModel;
570
- if (!model) {
571
- throw new Error(`agent role "${r.id}" has no model \u2014 provide defaultModel in plugin or model in config.runtime.agentRoles.${r.id}`);
621
+ function nativeReadBlobBytesAtRef(repoPath, ref, path) {
622
+ const requestDir = resolve3(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
623
+ mkdirSync2(requestDir, { recursive: true });
624
+ const outputPath = resolve3(requestDir, "blob.bin");
625
+ try {
626
+ requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
627
+ return readFileSync3(outputPath);
628
+ } finally {
629
+ rmSync(requestDir, { recursive: true, force: true });
630
+ }
631
+ }
632
+ function serializeTreeCommitUpdates(updates) {
633
+ return updates.map((update) => {
634
+ if (isTextTreeCommitUpdate(update)) {
635
+ return { path: update.path, kind: "text", content: update.content };
572
636
  }
573
- map.set(r.id, { ...r, model });
637
+ if (!isAbsolute(update.sourceFilePath)) {
638
+ throw new Error("tree commit binary updates require an absolute sourceFilePath");
639
+ }
640
+ return { path: update.path, kind: "file", sourceFilePath: update.sourceFilePath };
641
+ });
642
+ }
643
+ function buildTreeCommitUpdatesJson(updates) {
644
+ return `${JSON.stringify(serializeTreeCommitUpdates(updates), null, 2)}
645
+ `;
646
+ }
647
+ function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
648
+ const requestDir = resolve3(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
649
+ mkdirSync2(requestDir, { recursive: true });
650
+ const messagePath = resolve3(requestDir, "message.txt");
651
+ const updatesPath = resolve3(requestDir, "updates.json");
652
+ try {
653
+ writeFileSync2(messagePath, message, "utf8");
654
+ writeFileSync2(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
655
+ return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
656
+ } finally {
657
+ rmSync(requestDir, { recursive: true, force: true });
574
658
  }
575
- return {
576
- resolve(id) {
577
- const r = map.get(id);
578
- if (!r)
579
- throw new Error(`agent role not registered: ${id}`);
580
- return r;
581
- },
582
- list: () => Array.from(map.values())
583
- };
584
659
  }
585
-
586
- // packages/runtime/src/control-plane/task-fields.ts
587
- function createTaskFieldRegistry(extensions) {
588
- const byId = new Map;
589
- for (const e of extensions) {
590
- if (byId.has(e.id))
591
- throw new Error(`task field extension already registered: ${e.id}`);
592
- byId.set(e.id, e);
593
- }
594
- return {
595
- get: (id) => byId.get(id),
596
- list: () => Array.from(byId.values()),
597
- fieldNames: () => Array.from(byId.values()).map((e) => e.fieldName),
598
- validateTaskFields(task) {
599
- const errors = [];
600
- for (const ext of byId.values()) {
601
- let schema;
602
- try {
603
- schema = JSON.parse(ext.schemaJson);
604
- } catch {
605
- errors.push(`task field "${ext.id}": schemaJson is not valid JSON`);
606
- continue;
607
- }
608
- const isRequired = typeof schema === "object" && schema !== null && schema.required === true;
609
- if (!isRequired)
610
- continue;
611
- const value = task[ext.fieldName];
612
- if (value === undefined || value === null || value === "") {
613
- errors.push(`task field "${ext.fieldName}" (from extension "${ext.id}") is required but missing`);
614
- }
615
- }
616
- return errors.length === 0 ? { ok: true } : { ok: false, errors };
617
- }
618
- };
619
- }
620
-
621
- // packages/runtime/src/control-plane/validators/runtime-registration.ts
622
- import { existsSync as existsSync3 } from "fs";
623
- import { join } from "path";
624
- function createValidatorRegistry() {
625
- const map = new Map;
626
- const order = [];
627
- const registry = {
628
- register(v) {
629
- if (map.has(v.id))
630
- throw new Error(`validator already registered: ${v.id}`);
631
- map.set(v.id, v);
632
- order.push(v);
633
- },
634
- resolve(id) {
635
- const v = map.get(id);
636
- if (!v)
637
- throw new Error(`validator not registered: ${id}`);
638
- return v;
639
- },
640
- list: () => order
641
- };
642
- registerBuiltInValidators(registry);
643
- return registry;
644
- }
645
- function registerBuiltInValidators(registry) {
646
- registry.register({
647
- id: "std:typecheck",
648
- category: "custom",
649
- description: "Runs the package typecheck script when present.",
650
- run: async (ctx) => runStdTypecheck(ctx)
651
- });
652
- }
653
- async function runStdTypecheck(ctx) {
654
- const packageJsonPath = join(ctx.workspaceRoot, "package.json");
655
- if (!existsSync3(packageJsonPath)) {
656
- return {
657
- id: "std:typecheck",
658
- passed: false,
659
- summary: `package.json not found at ${packageJsonPath}`
660
- };
661
- }
662
- const proc = Bun.spawn(["bun", "run", "typecheck"], {
663
- cwd: ctx.workspaceRoot,
664
- env: process.env,
665
- stdout: "pipe",
666
- stderr: "pipe"
667
- });
668
- const [exitCode, stdout, stderr] = await Promise.all([
669
- proc.exited,
670
- new Response(proc.stdout).text(),
671
- new Response(proc.stderr).text()
660
+ function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
661
+ return requireGitNativeString("push-ref-with-lease", [
662
+ repoPath,
663
+ localOid,
664
+ remoteRef,
665
+ expectedOldOid,
666
+ remote
672
667
  ]);
673
- const output = `${stdout}${stderr}`.trim();
674
- return {
675
- id: "std:typecheck",
676
- passed: exitCode === 0,
677
- summary: exitCode === 0 ? "typecheck passed" : "typecheck failed",
678
- ...output ? { details: output.slice(0, 4000) } : {}
679
- };
680
668
  }
681
669
 
682
- // packages/runtime/src/control-plane/native/scope-rules.ts
683
- var activeRules = null;
684
- function setScopeRules(rules) {
685
- activeRules = rules ?? null;
670
+ // packages/runtime/src/control-plane/runtime/tooling/shell.ts
671
+ import { tmpdir as tmpdir2 } from "os";
672
+ import { basename, dirname as dirname3, resolve as resolve4 } from "path";
673
+ var sharedNativeShellOutputDir = resolve4(tmpdir2(), "rig-native");
674
+ var sharedNativeShellOutputPath = resolve4(sharedNativeShellOutputDir, `rig-shell-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
675
+ function runtimeToolGatewayNames() {
676
+ return [
677
+ "bash",
678
+ "sh",
679
+ "zsh",
680
+ "git",
681
+ "bun",
682
+ "node",
683
+ "python3",
684
+ "rg",
685
+ "grep",
686
+ "sed",
687
+ "cat",
688
+ "ls",
689
+ "find",
690
+ "tsc",
691
+ "gh",
692
+ "mkdir",
693
+ "rm",
694
+ "mv",
695
+ "cp",
696
+ "touch",
697
+ "pwd",
698
+ "head",
699
+ "tail",
700
+ "wc",
701
+ "sort",
702
+ "uniq",
703
+ "awk",
704
+ "xargs",
705
+ "dirname",
706
+ "basename",
707
+ "realpath",
708
+ "env",
709
+ "jq",
710
+ "tee",
711
+ "which"
712
+ ];
686
713
  }
687
- function getScopeRules() {
688
- return activeRules;
714
+ // packages/runtime/src/control-plane/runtime/tooling/file-tools.ts
715
+ import { tmpdir as tmpdir3 } from "os";
716
+ import { basename as basename2, dirname as dirname4, resolve as resolve5 } from "path";
717
+ var sharedNativeToolsOutputDir = resolve5(tmpdir3(), "rig-native");
718
+ var sharedNativeToolsOutputPath = resolve5(sharedNativeToolsOutputDir, `rig-tools-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
719
+ function runtimeFileToolNames() {
720
+ return [
721
+ "rig-read",
722
+ "rig-write",
723
+ "rig-edit",
724
+ "rig-glob",
725
+ "rig-grep"
726
+ ];
689
727
  }
690
728
 
691
- // packages/runtime/src/control-plane/hook-materializer.ts
692
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
693
- import { dirname as dirname4, resolve as resolve6 } from "path";
694
- var MARKER_PLUGIN = "_rigPlugin";
695
- var MARKER_HOOK_ID = "_rigHookId";
696
- function matcherToString(matcher) {
697
- if (matcher.kind === "all")
698
- return;
699
- if (matcher.kind === "tool")
700
- return matcher.name;
701
- return matcher.pattern;
702
- }
703
- function isPluginOwned(cmd) {
704
- return typeof cmd[MARKER_PLUGIN] === "string";
705
- }
706
- function materializeHooks(projectRoot, entries) {
707
- const settingsPath = resolve6(projectRoot, ".claude", "settings.json");
708
- const existing = existsSync4(settingsPath) ? safeReadJson(settingsPath) : {};
709
- const hooks = existing.hooks ?? {};
710
- for (const event of Object.keys(hooks)) {
711
- const groups = hooks[event] ?? [];
712
- const cleaned = [];
713
- for (const group of groups) {
714
- const operatorHooks = group.hooks.filter((h) => !isPluginOwned(h));
715
- if (operatorHooks.length > 0) {
716
- cleaned.push({ ...group, hooks: operatorHooks });
717
- }
718
- }
719
- if (cleaned.length > 0) {
720
- hooks[event] = cleaned;
721
- } else {
722
- delete hooks[event];
723
- }
724
- }
725
- for (const { pluginName, hook } of entries) {
726
- if (!hook.command) {
727
- continue;
728
- }
729
- const event = hook.event;
730
- const matcherString = matcherToString(hook.matcher);
731
- const groups = hooks[event] ??= [];
732
- let group = groups.find((g) => g.matcher === matcherString);
733
- if (!group) {
734
- group = matcherString === undefined ? { hooks: [] } : { matcher: matcherString, hooks: [] };
735
- groups.push(group);
736
- }
737
- group.hooks.push({
738
- type: "command",
739
- command: hook.command,
740
- [MARKER_PLUGIN]: pluginName,
741
- [MARKER_HOOK_ID]: hook.id
742
- });
743
- }
744
- const next = { ...existing };
745
- if (Object.keys(hooks).length > 0) {
746
- next.hooks = hooks;
747
- } else {
748
- delete next.hooks;
749
- }
750
- mkdirSync2(dirname4(settingsPath), { recursive: true });
751
- writeFileSync2(settingsPath, `${JSON.stringify(next, null, 2)}
752
- `, "utf-8");
753
- return settingsPath;
729
+ // packages/runtime/src/control-plane/runtime/tooling/gateway.ts
730
+ function runtimeGatewayToolNames() {
731
+ return runtimeToolGatewayNames();
754
732
  }
755
- function safeReadJson(path) {
756
- try {
757
- return JSON.parse(readFileSync3(path, "utf-8"));
758
- } catch {
759
- return {};
733
+ // packages/runtime/src/control-plane/browser-contract.ts
734
+ import { resolve as resolve6 } from "path";
735
+ var DEFAULT_BROWSER_ATTACH_URL = "http://127.0.0.1:9333";
736
+ var DEFAULT_BROWSER_MODE = "persistent";
737
+ var RUNTIME_BROWSER_HELPERS = {
738
+ launch: "rig-browser-launch",
739
+ check: "rig-browser-check",
740
+ attachInfo: "rig-browser-attach-info",
741
+ e2e: "rig-browser-e2e",
742
+ resetProfile: "rig-browser-reset-profile"
743
+ };
744
+ var BASE_REMOTE_DEBUGGING_PORT = 9222;
745
+ var REMOTE_DEBUGGING_PORT_SPREAD = 4000;
746
+ function hashString(input) {
747
+ let hash = 0;
748
+ for (let index = 0;index < input.length; index += 1) {
749
+ hash = (hash << 5) - hash + input.charCodeAt(index) | 0;
760
750
  }
751
+ return Math.abs(hash);
761
752
  }
762
-
763
- // packages/runtime/src/control-plane/skill-materializer.ts
764
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
765
- import { resolve as resolve7 } from "path";
766
- import { loadSkill } from "@rig/skill-loader";
767
- var MARKER_FILENAME = ".rig-plugin";
768
- function skillDirName(id) {
769
- return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
770
- }
771
- async function materializeSkills(projectRoot, entries) {
772
- const skillsRoot = resolve7(projectRoot, ".pi", "skills");
773
- if (existsSync5(skillsRoot)) {
774
- for (const name of readdirSync(skillsRoot)) {
775
- const dir = resolve7(skillsRoot, name);
776
- if (existsSync5(resolve7(dir, MARKER_FILENAME))) {
777
- rmSync(dir, { recursive: true, force: true });
778
- }
779
- }
780
- }
781
- const written = [];
782
- for (const { pluginName, skill } of entries) {
783
- const sourcePath = resolve7(projectRoot, skill.path);
784
- if (!existsSync5(sourcePath)) {
785
- console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
786
- continue;
787
- }
788
- let body;
789
- try {
790
- await loadSkill(sourcePath);
791
- body = readFileSync4(sourcePath, "utf-8");
792
- } catch (err) {
793
- console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
794
- continue;
795
- }
796
- const dir = resolve7(skillsRoot, skillDirName(skill.id));
797
- mkdirSync3(dir, { recursive: true });
798
- writeFileSync3(resolve7(dir, "SKILL.md"), body, "utf-8");
799
- writeFileSync3(resolve7(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
800
- `, "utf-8");
801
- written.push({ id: skill.id, pluginName, directory: dir });
802
- }
803
- return written;
753
+ function derivePortFromProfile(profileName) {
754
+ return BASE_REMOTE_DEBUGGING_PORT + hashString(profileName) % REMOTE_DEBUGGING_PORT_SPREAD;
804
755
  }
805
-
806
- // packages/runtime/src/control-plane/plugin-host-context.ts
807
- async function buildPluginHostContext(projectRoot) {
808
- let config;
809
- try {
810
- config = await loadConfig(projectRoot);
811
- } catch (err) {
812
- const msg = err instanceof Error ? err.message : String(err);
813
- if (msg.includes("no rig.config")) {
814
- return null;
815
- }
816
- throw err;
817
- }
818
- const pluginHost = createPluginHost(config.plugins);
819
- setScopeRules(config.workspace.scopeNormalization);
820
- const validatorRegistry = createValidatorRegistry();
821
- for (const impl of pluginHost.listExecutableValidators()) {
822
- validatorRegistry.register(impl);
823
- }
824
- const taskSourceRegistry = buildTaskSourceRegistry(config, pluginHost);
825
- const repoRegistry = createRepoRegistry(pluginHost.listRepoSources());
826
- const managedEntries = pluginHost.listRepoSources().map(repoRegistrationToManagedEntry).filter((e) => e !== null);
827
- setManagedRepos(managedEntries);
828
- const configRoleOverrides = config.runtime?.agentRoles;
829
- const agentRoleRegistry = createAgentRoleRegistry(pluginHost.listAgentRoles(), configRoleOverrides);
830
- const taskFieldRegistry = createTaskFieldRegistry(pluginHost.listTaskFieldExtensions());
831
- try {
832
- const hookEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.hooks ?? []).map((hook) => ({
833
- pluginName: plugin.name,
834
- hook
835
- })));
836
- if (hookEntries.length > 0) {
837
- materializeHooks(projectRoot, hookEntries);
838
- }
839
- } catch (err) {
840
- console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
841
- }
756
+ function parseAttachUrl(attachUrl) {
842
757
  try {
843
- const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
844
- pluginName: plugin.name,
845
- skill
846
- })));
847
- if (skillEntries.length > 0) {
848
- await materializeSkills(projectRoot, skillEntries);
849
- }
850
- } catch (err) {
851
- console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
852
- }
853
- return {
854
- config,
855
- pluginHost,
856
- validatorRegistry,
857
- taskSourceRegistry,
858
- repoRegistry,
859
- agentRoleRegistry,
860
- taskFieldRegistry
861
- };
862
- }
863
-
864
- // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
865
- import { spawnSync } from "child_process";
866
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync4 } from "fs";
867
- import { basename as basename3, join as join2, resolve as resolve9 } from "path";
868
-
869
- // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
870
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
871
- import { resolve as resolve8 } from "path";
872
-
873
- // packages/runtime/src/control-plane/tasks/task-record-reader.ts
874
- async function findTaskById(reader, id) {
875
- const tasks = await reader.listTasks();
876
- return tasks.find((task) => task.id === id) ?? null;
877
- }
878
-
879
- // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
880
- class LegacyTaskConfigReadError extends Error {
881
- code = "LEGACY_TASK_CONFIG_READ_FAILED";
882
- projectRoot;
883
- configPath;
884
- cause;
885
- constructor(input) {
886
- super(input.message, { cause: input.cause });
887
- this.name = "LegacyTaskConfigReadError";
888
- this.projectRoot = input.projectRoot;
889
- this.configPath = input.configPath;
890
- this.cause = input.cause;
758
+ return new URL((attachUrl || DEFAULT_BROWSER_ATTACH_URL).trim() || DEFAULT_BROWSER_ATTACH_URL);
759
+ } catch {
760
+ return new URL(DEFAULT_BROWSER_ATTACH_URL);
891
761
  }
892
762
  }
893
- function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
894
- const configPath = options.configPath ?? resolve8(projectRoot, ".rig", "task-config.json");
895
- const reader = {
896
- async listTasks() {
897
- return readLegacyTaskRecords(projectRoot, configPath);
898
- },
899
- async getTask(id) {
900
- return findTaskById(reader, id);
901
- }
902
- };
903
- return reader;
904
- }
905
- function readLegacyTaskRecords(projectRoot, configPath = resolve8(projectRoot, ".rig", "task-config.json")) {
906
- if (!existsSync6(configPath)) {
907
- return [];
908
- }
909
- const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
910
- return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
763
+ function sanitizeRuntimeSuffix(runtimeId) {
764
+ return runtimeId.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 16) || "runtime";
911
765
  }
912
- function readLegacyTaskConfigJson(projectRoot, configPath) {
913
- try {
914
- const parsed = JSON.parse(readFileSync5(configPath, "utf8"));
915
- if (isPlainRecord(parsed)) {
916
- return parsed;
917
- }
918
- throw new Error("task config root must be a JSON object");
919
- } catch (cause) {
920
- throw new LegacyTaskConfigReadError({
921
- projectRoot,
922
- configPath,
923
- message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
924
- cause
925
- });
766
+ function resolveBrowserStateDir(projectRoot, configuredStateDir) {
767
+ const trimmed = configuredStateDir?.trim() || ".tmp/rig-browser";
768
+ if (trimmed.startsWith("/")) {
769
+ return resolve6(trimmed);
926
770
  }
771
+ return resolve6(projectRoot || process.cwd(), trimmed);
927
772
  }
928
- function stripLegacyTaskConfigMetadata(raw) {
929
- const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
930
- return tasks;
931
- }
932
- function legacyTaskConfigEntryToRecord(id, entry) {
933
- if (!isPlainRecord(entry)) {
934
- return null;
773
+ function resolveTaskBrowserContext(browser, options = {}) {
774
+ if (!browser?.required) {
775
+ return;
935
776
  }
936
- const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
937
- const validation = readStringList(entry.validation);
938
- const validators = readStringList(entry.validators);
939
- const scope = readStringList(entry.scope);
940
- const status = typeof entry.status === "string" ? entry.status : "open";
941
- const title = typeof entry.title === "string" ? entry.title : undefined;
942
- const description = typeof entry.description === "string" ? entry.description : undefined;
943
- const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
777
+ const defaultProfile = browser.profile?.trim() || "default";
778
+ const mode = browser.mode?.trim() || DEFAULT_BROWSER_MODE;
779
+ const defaultAttach = parseAttachUrl(browser.attach_url);
780
+ const shouldDeriveRuntimeProfile = Boolean(options.runtimeId?.trim()) && mode !== "shared";
781
+ const effectiveProfile = shouldDeriveRuntimeProfile ? `${defaultProfile}-${sanitizeRuntimeSuffix(options.runtimeId.trim())}` : defaultProfile;
782
+ const effectivePort = shouldDeriveRuntimeProfile ? derivePortFromProfile(effectiveProfile) : Number(defaultAttach.port || "80");
783
+ const effectiveAttach = new URL(defaultAttach.toString());
784
+ effectiveAttach.port = String(effectivePort);
944
785
  return {
945
- id,
946
- deps,
947
- status,
948
- source: "legacy-task-config",
949
- ...title ? { title } : {},
950
- ...description ? { description } : {},
951
- ...acceptanceCriteria ? { acceptanceCriteria } : {},
952
- ...scope.length > 0 ? { scope } : {},
953
- ...validation.length > 0 ? { validation } : {},
954
- ...validators.length > 0 ? { validators } : {},
955
- ...preservedLegacyFields(entry)
956
- };
957
- }
958
- function preservedLegacyFields(entry) {
959
- const preserved = {};
960
- for (const key of [
961
- "role",
962
- "browser",
963
- "repo_pins",
964
- "criticality",
965
- "queue_weight",
966
- "creates_repo",
967
- "auto_synced"
968
- ]) {
969
- if (entry[key] !== undefined) {
970
- preserved[key] = entry[key];
971
- }
972
- }
973
- return preserved;
786
+ required: true,
787
+ preset: browser.preset?.trim() || "default",
788
+ mode,
789
+ stateDir: resolveBrowserStateDir(options.hostProjectRoot, browser.state_dir),
790
+ defaultProfile,
791
+ effectiveProfile,
792
+ defaultAttachUrl: defaultAttach.toString(),
793
+ effectiveAttachUrl: effectiveAttach.toString(),
794
+ devCommand: browser.dev_command?.trim() || undefined,
795
+ launchCommand: browser.launch_command?.trim() || undefined,
796
+ checkCommand: browser.check_command?.trim() || undefined,
797
+ e2eCommand: browser.e2e_command?.trim() || undefined,
798
+ launchHelper: RUNTIME_BROWSER_HELPERS.launch,
799
+ checkHelper: RUNTIME_BROWSER_HELPERS.check,
800
+ attachInfoHelper: RUNTIME_BROWSER_HELPERS.attachInfo,
801
+ e2eHelper: RUNTIME_BROWSER_HELPERS.e2e,
802
+ resetProfileHelper: RUNTIME_BROWSER_HELPERS.resetProfile
803
+ };
974
804
  }
975
- function firstStringList(...candidates) {
976
- for (const candidate of candidates) {
977
- const list = readStringList(candidate);
978
- if (list.length > 0) {
979
- return list;
980
- }
805
+ function buildBrowserGuidanceLines(browser) {
806
+ const lines = [
807
+ "This task requires Rig Browser.",
808
+ `Launch the browser: \`${browser.launchHelper}\`${browser.devCommand ? " or `rig-browser-launch --dev`" : ""}.`,
809
+ `Check the browser contract: \`${browser.checkHelper}\`.`,
810
+ `Show attach details: \`${browser.attachInfoHelper}\`.`,
811
+ `Attach Chrome DevTools MCP to ${browser.effectiveAttachUrl}.`,
812
+ `Preset: ${browser.preset}.`,
813
+ `Profile: ${browser.effectiveProfile}.`,
814
+ `State dir: ${browser.stateDir}.`,
815
+ `Reset the active profile with \`${browser.resetProfileHelper}\`.`
816
+ ];
817
+ if (browser.e2eCommand) {
818
+ lines.push(`Run app-owned browser e2e with \`${browser.e2eHelper}\`.`);
981
819
  }
982
- return [];
983
- }
984
- function readStringList(candidate) {
985
- if (!Array.isArray(candidate)) {
986
- return [];
820
+ if (browser.defaultProfile !== browser.effectiveProfile) {
821
+ lines.push(`Base profile: ${browser.defaultProfile}. Runtime launches derive an isolated effective profile.`);
987
822
  }
988
- return candidate.filter((value) => typeof value === "string");
989
- }
990
- function isPlainRecord(candidate) {
991
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
823
+ return lines;
992
824
  }
993
825
 
994
- // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
995
- var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
996
- var FILE_TASK_PATTERN = /\.(task\.)?json$/;
997
- function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
998
- const configPath = options.configPath ?? resolve9(projectRoot, ".rig", "task-config.json");
999
- const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
1000
- const spawnFn = options.spawn ?? spawnSync;
1001
- const ghBinary = options.ghBinary ?? "gh";
1002
- const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
826
+ // packages/runtime/src/control-plane/plugin-host-context.ts
827
+ import { createPluginHost } from "@rig/core";
828
+ import { loadConfig } from "@rig/core/load-config";
829
+
830
+ // packages/runtime/src/control-plane/task-source.ts
831
+ function createTaskSourceRegistry() {
832
+ const byId = new Map;
833
+ const order = [];
1003
834
  return {
1004
- async listTasks() {
1005
- const rawConfig = readRawTaskConfig(configPath);
1006
- if (!rawConfig) {
1007
- const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1008
- return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
1009
- }
1010
- const tasks = [];
1011
- const legacyTasks = await legacy.listTasks();
1012
- const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
1013
- for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
1014
- if (!isPlainRecord2(rawEntry)) {
1015
- continue;
1016
- }
1017
- const metadata = readMaterializedTaskMetadata(rawEntry);
1018
- if (metadata.taskSource?.kind === "github-issues") {
1019
- tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
1020
- continue;
1021
- }
1022
- if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1023
- const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1024
- if (fileTask)
1025
- tasks.push(fileTask);
1026
- continue;
1027
- }
1028
- if (!allowLocalFallback) {
1029
- continue;
1030
- }
1031
- const legacyTask = legacyById.get(id);
1032
- if (legacyTask) {
1033
- tasks.push(legacyTask);
1034
- }
1035
- }
1036
- return tasks;
835
+ register(s) {
836
+ if (byId.has(s.id))
837
+ throw new Error(`task source already registered: ${s.id}`);
838
+ byId.set(s.id, s);
839
+ order.push(s);
1037
840
  },
1038
- async getTask(id) {
1039
- const rawEntry = readRawTaskEntry(configPath, id);
1040
- if (!rawEntry) {
1041
- const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1042
- return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
1043
- }
1044
- const metadata = readMaterializedTaskMetadata(rawEntry);
1045
- if (metadata.taskSource?.kind === "github-issues") {
1046
- return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
1047
- }
1048
- if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1049
- return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1050
- }
1051
- return allowLocalFallback ? legacy.getTask(id) : null;
1052
- }
841
+ resolveById(id) {
842
+ const s = byId.get(id);
843
+ if (!s)
844
+ throw new Error(`task source not registered: ${id}`);
845
+ return s;
846
+ },
847
+ resolveByKind(kind) {
848
+ for (const s of order)
849
+ if (s.kind === kind)
850
+ return s;
851
+ throw new Error(`no task source registered for kind: ${kind}`);
852
+ },
853
+ list: () => order
1053
854
  };
1054
855
  }
1055
- function readMaterializedTaskMetadata(entry) {
1056
- const rawRig = entry._rig;
1057
- if (!isPlainRecord2(rawRig)) {
1058
- return {};
1059
- }
1060
- const rawSource = rawRig.taskSource;
1061
- const metadata = {};
1062
- if (isPlainRecord2(rawSource)) {
1063
- const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
1064
- if (kind.length > 0) {
1065
- metadata.taskSource = {
1066
- kind,
1067
- ...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
1068
- ...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
1069
- ...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
1070
- ...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
1071
- ...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
1072
- };
1073
- }
1074
- }
1075
- if (typeof rawRig.sourceIssueId === "string") {
1076
- metadata.sourceIssueId = rawRig.sourceIssueId;
1077
- }
1078
- return metadata;
856
+
857
+ // packages/runtime/src/control-plane/task-source-bootstrap.ts
858
+ function formatRegisteredKinds(pluginHost) {
859
+ const kinds = pluginHost ? pluginHost.listExecutableTaskSources().map((source) => source.kind) : [];
860
+ return kinds.length > 0 ? kinds.join(", ") : "none";
1079
861
  }
1080
- function readConfiguredFilesTaskSourcePath(projectRoot) {
1081
- const jsonPath = resolve9(projectRoot, "rig.config.json");
1082
- if (existsSync7(jsonPath)) {
1083
- try {
1084
- const parsed = JSON.parse(readFileSync6(jsonPath, "utf8"));
1085
- if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
1086
- const source = parsed.taskSource;
1087
- return source.kind === "files" && typeof source.path === "string" ? source.path : null;
1088
- }
1089
- } catch {
1090
- return null;
1091
- }
1092
- }
1093
- const tsPath = resolve9(projectRoot, "rig.config.ts");
1094
- if (!existsSync7(tsPath)) {
1095
- return null;
862
+ function buildTaskSourceRegistry(config, pluginHost) {
863
+ const registry = createTaskSourceRegistry();
864
+ const taskSourceConfig = config.taskSource;
865
+ const factory = pluginHost?.resolveTaskSourceFactoryByKind(taskSourceConfig.kind);
866
+ if (!factory) {
867
+ 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.");
1096
868
  }
1097
- try {
1098
- const source = readFileSync6(tsPath, "utf8");
1099
- const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
1100
- const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
1101
- if (kind !== "files") {
1102
- return null;
1103
- }
1104
- return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
1105
- } catch {
1106
- return null;
869
+ registry.register(factory.factory(taskSourceConfig));
870
+ return registry;
871
+ }
872
+
873
+ // packages/runtime/src/control-plane/repos/registry.ts
874
+ function createRepoRegistry(entries) {
875
+ const map = new Map;
876
+ for (const e of entries) {
877
+ if (map.has(e.id))
878
+ throw new Error(`repo already registered: ${e.id}`);
879
+ map.set(e.id, { ...e });
1107
880
  }
881
+ const ordered = Array.from(map.values());
882
+ return {
883
+ getById: (id) => map.get(id),
884
+ list: () => ordered
885
+ };
1108
886
  }
1109
- function readRawTaskEntry(configPath, taskId) {
1110
- const rawConfig = readRawTaskConfig(configPath);
1111
- if (!rawConfig) {
1112
- return null;
887
+ var MANAGED_REPOS = new Map;
888
+ function setManagedRepos(entries) {
889
+ const next = new Map;
890
+ for (const e of entries) {
891
+ if (next.has(e.id)) {
892
+ throw new Error(`managed repo already registered: ${e.id}`);
893
+ }
894
+ next.set(e.id, e);
1113
895
  }
1114
- const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
1115
- return isPlainRecord2(entry) ? entry : null;
896
+ MANAGED_REPOS = next;
1116
897
  }
1117
- function readRawTaskConfig(configPath) {
1118
- if (!existsSync7(configPath)) {
1119
- return null;
898
+ function getManagedRepoEntry(repoId) {
899
+ const entry = MANAGED_REPOS.get(repoId);
900
+ if (!entry) {
901
+ 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.`);
1120
902
  }
1121
- const parsed = JSON.parse(readFileSync6(configPath, "utf8"));
1122
- return isPlainRecord2(parsed) ? parsed : null;
903
+ return entry;
1123
904
  }
1124
- function stripLegacyTaskConfigMetadata2(raw) {
1125
- const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1126
- return tasks;
905
+ function listManagedRepoEntries() {
906
+ return Array.from(MANAGED_REPOS.values());
1127
907
  }
1128
- function listFileBackedTasks(projectRoot, sourcePath) {
1129
- const directory = resolve9(projectRoot, sourcePath);
1130
- if (!existsSync7(directory)) {
1131
- return [];
1132
- }
1133
- const tasks = [];
1134
- for (const name of readdirSync2(directory)) {
1135
- if (!FILE_TASK_PATTERN.test(name))
1136
- continue;
1137
- const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
1138
- const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
1139
- if (task)
1140
- tasks.push(task);
908
+ function resolveManagedRepoIdByAlias(alias) {
909
+ for (const entry of MANAGED_REPOS.values()) {
910
+ if (entry.alias === alias) {
911
+ return entry.id;
912
+ }
1141
913
  }
1142
- return tasks;
914
+ return null;
1143
915
  }
1144
- function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
1145
- const file = findFileBackedTaskFile(resolve9(projectRoot, sourcePath), taskId);
1146
- if (!file) {
916
+ function repoRegistrationToManagedEntry(reg) {
917
+ if (!reg.defaultBranch) {
1147
918
  return null;
1148
919
  }
1149
- const raw = JSON.parse(readFileSync6(file, "utf8"));
1150
- if (!isPlainRecord2(raw)) {
1151
- return null;
920
+ return {
921
+ id: reg.id,
922
+ alias: reg.defaultPath ?? reg.id,
923
+ defaultBranch: reg.defaultBranch,
924
+ defaultRemoteUrl: reg.url,
925
+ remoteEnvVar: reg.remoteEnvVar,
926
+ checkoutEnvVar: reg.checkoutEnvVar
927
+ };
928
+ }
929
+
930
+ // packages/runtime/src/control-plane/agent-roles.ts
931
+ function createAgentRoleRegistry(pluginRoles, configOverrides) {
932
+ const map = new Map;
933
+ for (const r of pluginRoles) {
934
+ if (map.has(r.id))
935
+ throw new Error(`agent role already registered: ${r.id}`);
936
+ const override = configOverrides?.[r.id];
937
+ const model = override?.model ?? r.defaultModel;
938
+ if (!model) {
939
+ throw new Error(`agent role "${r.id}" has no model \u2014 provide defaultModel in plugin or model in config.runtime.agentRoles.${r.id}`);
940
+ }
941
+ map.set(r.id, { ...r, model });
1152
942
  }
1153
943
  return {
1154
- id: typeof raw.id === "string" ? raw.id : taskId,
1155
- deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
1156
- status: typeof raw.status === "string" ? raw.status : "ready",
1157
- title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
1158
- ...raw
944
+ resolve(id) {
945
+ const r = map.get(id);
946
+ if (!r)
947
+ throw new Error(`agent role not registered: ${id}`);
948
+ return r;
949
+ },
950
+ list: () => Array.from(map.values())
1159
951
  };
1160
952
  }
1161
- function findFileBackedTaskFile(directory, taskId) {
1162
- if (!existsSync7(directory)) {
1163
- return null;
953
+
954
+ // packages/runtime/src/control-plane/task-fields.ts
955
+ function createTaskFieldRegistry(extensions) {
956
+ const byId = new Map;
957
+ for (const e of extensions) {
958
+ if (byId.has(e.id))
959
+ throw new Error(`task field extension already registered: ${e.id}`);
960
+ byId.set(e.id, e);
1164
961
  }
1165
- for (const name of readdirSync2(directory)) {
1166
- if (!FILE_TASK_PATTERN.test(name))
1167
- continue;
1168
- const file = join2(directory, name);
1169
- try {
1170
- if (!statSync(file).isFile())
1171
- continue;
1172
- const raw = JSON.parse(readFileSync6(file, "utf8"));
1173
- const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
1174
- const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
1175
- if (id === taskId) {
1176
- return file;
962
+ return {
963
+ get: (id) => byId.get(id),
964
+ list: () => Array.from(byId.values()),
965
+ fieldNames: () => Array.from(byId.values()).map((e) => e.fieldName),
966
+ validateTaskFields(task) {
967
+ const errors = [];
968
+ for (const ext of byId.values()) {
969
+ let schema;
970
+ try {
971
+ schema = JSON.parse(ext.schemaJson);
972
+ } catch {
973
+ errors.push(`task field "${ext.id}": schemaJson is not valid JSON`);
974
+ continue;
975
+ }
976
+ const isRequired = typeof schema === "object" && schema !== null && schema.required === true;
977
+ if (!isRequired)
978
+ continue;
979
+ const value = task[ext.fieldName];
980
+ if (value === undefined || value === null || value === "") {
981
+ errors.push(`task field "${ext.fieldName}" (from extension "${ext.id}") is required but missing`);
982
+ }
1177
983
  }
1178
- } catch {}
1179
- }
1180
- return null;
984
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
985
+ }
986
+ };
1181
987
  }
1182
- function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
1183
- const source = requireGithubIssueSource(metadata, id);
1184
- const issue = runGh(bin, [
1185
- "issue",
1186
- "view",
1187
- String(id),
1188
- "--repo",
1189
- `${source.owner}/${source.repo}`,
1190
- "--json",
1191
- "number,title,body,labels,state,url,assignees"
1192
- ], spawnFn);
1193
- return githubIssueToTask(issue, source, rawEntry);
988
+
989
+ // packages/runtime/src/control-plane/validators/runtime-registration.ts
990
+ import { existsSync as existsSync4 } from "fs";
991
+ import { join } from "path";
992
+ function createValidatorRegistry() {
993
+ const map = new Map;
994
+ const order = [];
995
+ const registry = {
996
+ register(v) {
997
+ if (map.has(v.id))
998
+ throw new Error(`validator already registered: ${v.id}`);
999
+ map.set(v.id, v);
1000
+ order.push(v);
1001
+ },
1002
+ resolve(id) {
1003
+ const v = map.get(id);
1004
+ if (!v)
1005
+ throw new Error(`validator not registered: ${id}`);
1006
+ return v;
1007
+ },
1008
+ list: () => order
1009
+ };
1010
+ registerBuiltInValidators(registry);
1011
+ return registry;
1194
1012
  }
1195
- function requireGithubIssueSource(metadata, id) {
1196
- const source = metadata.taskSource;
1197
- if (source?.kind === "github-issues" && source.owner && source.repo) {
1198
- return { owner: source.owner, repo: source.repo };
1199
- }
1200
- const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
1201
- if (parsed && parsed[3] === id) {
1202
- return { owner: parsed[1], repo: parsed[2] };
1203
- }
1204
- throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
1013
+ function registerBuiltInValidators(registry) {
1014
+ registry.register({
1015
+ id: "std:typecheck",
1016
+ category: "custom",
1017
+ description: "Runs the package typecheck script when present.",
1018
+ run: async (ctx) => runStdTypecheck(ctx)
1019
+ });
1205
1020
  }
1206
- function githubIssueToTask(issue, source, rawEntry) {
1207
- const labelNames = (issue.labels ?? []).map((label) => label.name);
1208
- const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
1209
- const roleLabel = labelNames.find((label) => label.startsWith("role:"));
1210
- const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
1211
- const body = issue.body ?? "";
1212
- const repo = `${source.owner}/${source.repo}`;
1021
+ async function runStdTypecheck(ctx) {
1022
+ const packageJsonPath = join(ctx.workspaceRoot, "package.json");
1023
+ if (!existsSync4(packageJsonPath)) {
1024
+ return {
1025
+ id: "std:typecheck",
1026
+ passed: false,
1027
+ summary: `package.json not found at ${packageJsonPath}`
1028
+ };
1029
+ }
1030
+ const proc = Bun.spawn(["bun", "run", "typecheck"], {
1031
+ cwd: ctx.workspaceRoot,
1032
+ env: process.env,
1033
+ stdout: "pipe",
1034
+ stderr: "pipe"
1035
+ });
1036
+ const [exitCode, stdout, stderr] = await Promise.all([
1037
+ proc.exited,
1038
+ new Response(proc.stdout).text(),
1039
+ new Response(proc.stderr).text()
1040
+ ]);
1041
+ const output = `${stdout}${stderr}`.trim();
1213
1042
  return {
1214
- id: String(issue.number),
1215
- deps: parseDeps(body),
1216
- status: githubStatusFor(issue),
1217
- title: issue.title,
1218
- body,
1219
- ...scope.length > 0 ? { scope } : {},
1220
- ...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
1221
- ...validators.length > 0 ? { validators } : {},
1222
- ...issue.url ? { url: issue.url } : {},
1223
- issueType: issueTypeFor(labelNames),
1224
- sourceIssueId: `${repo}#${issue.number}`,
1225
- parentChildDeps: parseParents(body),
1226
- labels: labelNames,
1227
- raw: issue,
1228
- source: "github-issues",
1229
- _rig: {
1230
- taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
1231
- sourceIssueId: `${repo}#${issue.number}`
1232
- }
1043
+ id: "std:typecheck",
1044
+ passed: exitCode === 0,
1045
+ summary: exitCode === 0 ? "typecheck passed" : "typecheck failed",
1046
+ ...output ? { details: output.slice(0, 4000) } : {}
1233
1047
  };
1234
1048
  }
1235
- function githubStatusFor(issue) {
1236
- const state = (issue.state ?? "").toUpperCase();
1237
- if (state === "CLOSED")
1238
- return "closed";
1239
- const labelNames = (issue.labels ?? []).map((label) => label.name);
1240
- if (labelNames.includes("in-progress"))
1241
- return "in_progress";
1242
- if (labelNames.includes("blocked"))
1243
- return "blocked";
1244
- if (labelNames.includes("ready"))
1245
- return "ready";
1246
- if (labelNames.includes("under-review"))
1247
- return "under_review";
1248
- if (labelNames.includes("failed"))
1249
- return "failed";
1250
- if (labelNames.includes("cancelled"))
1251
- return "cancelled";
1252
- return "open";
1049
+
1050
+ // packages/runtime/src/control-plane/native/scope-rules.ts
1051
+ var activeRules = null;
1052
+ function setScopeRules(rules) {
1053
+ activeRules = rules ?? null;
1253
1054
  }
1254
- function selectedGitHubEnv() {
1255
- const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1256
- return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1055
+ function getScopeRules() {
1056
+ return activeRules;
1257
1057
  }
1258
- function ghSpawnOptions() {
1259
- return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
1058
+
1059
+ // packages/runtime/src/control-plane/hook-materializer.ts
1060
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1061
+ import { dirname as dirname5, resolve as resolve7 } from "path";
1062
+ var MARKER_PLUGIN = "_rigPlugin";
1063
+ var MARKER_HOOK_ID = "_rigHookId";
1064
+ function matcherToString(matcher) {
1065
+ if (matcher.kind === "all")
1066
+ return;
1067
+ if (matcher.kind === "tool")
1068
+ return matcher.name;
1069
+ return matcher.pattern;
1260
1070
  }
1261
- function runGh(bin, args, spawnFn) {
1262
- const res = spawnFn(bin, [...args], ghSpawnOptions());
1263
- assertGhSuccess(args, res);
1264
- if (!res.stdout || res.stdout.trim() === "") {
1265
- throw new Error(`gh ${args.join(" ")} returned empty stdout`);
1266
- }
1267
- return JSON.parse(res.stdout);
1071
+ function isPluginOwned(cmd) {
1072
+ return typeof cmd[MARKER_PLUGIN] === "string";
1268
1073
  }
1269
- function assertGhSuccess(args, res) {
1270
- if (res.error) {
1271
- const msg = res.error.message ?? String(res.error);
1272
- throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
1074
+ function materializeHooks(projectRoot, entries) {
1075
+ const settingsPath = resolve7(projectRoot, ".claude", "settings.json");
1076
+ const existing = existsSync5(settingsPath) ? safeReadJson(settingsPath) : {};
1077
+ const hooks = existing.hooks ?? {};
1078
+ for (const event of Object.keys(hooks)) {
1079
+ const groups = hooks[event] ?? [];
1080
+ const cleaned = [];
1081
+ for (const group of groups) {
1082
+ const operatorHooks = group.hooks.filter((h) => !isPluginOwned(h));
1083
+ if (operatorHooks.length > 0) {
1084
+ cleaned.push({ ...group, hooks: operatorHooks });
1085
+ }
1086
+ }
1087
+ if (cleaned.length > 0) {
1088
+ hooks[event] = cleaned;
1089
+ } else {
1090
+ delete hooks[event];
1091
+ }
1092
+ }
1093
+ for (const { pluginName, hook } of entries) {
1094
+ if (!hook.command) {
1095
+ continue;
1096
+ }
1097
+ const event = hook.event;
1098
+ const matcherString = matcherToString(hook.matcher);
1099
+ const groups = hooks[event] ??= [];
1100
+ let group = groups.find((g) => g.matcher === matcherString);
1101
+ if (!group) {
1102
+ group = matcherString === undefined ? { hooks: [] } : { matcher: matcherString, hooks: [] };
1103
+ groups.push(group);
1104
+ }
1105
+ group.hooks.push({
1106
+ type: "command",
1107
+ command: hook.command,
1108
+ [MARKER_PLUGIN]: pluginName,
1109
+ [MARKER_HOOK_ID]: hook.id
1110
+ });
1273
1111
  }
1274
- if (res.status !== 0) {
1275
- throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
1112
+ const next = { ...existing };
1113
+ if (Object.keys(hooks).length > 0) {
1114
+ next.hooks = hooks;
1115
+ } else {
1116
+ delete next.hooks;
1276
1117
  }
1118
+ mkdirSync3(dirname5(settingsPath), { recursive: true });
1119
+ writeFileSync3(settingsPath, `${JSON.stringify(next, null, 2)}
1120
+ `, "utf-8");
1121
+ return settingsPath;
1277
1122
  }
1278
- function parseDeps(body) {
1279
- return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
1280
- }
1281
- function parseParents(body) {
1282
- return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
1283
- }
1284
- function parseIssueRefs(body, pattern) {
1285
- const match = body.match(pattern);
1286
- if (!match)
1287
- return [];
1288
- return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
1289
- }
1290
- function issueTypeFor(labels) {
1291
- const typed = labels.find((label) => label.startsWith("type:"));
1292
- if (typed)
1293
- return typed.slice("type:".length);
1294
- if (labels.includes("epic"))
1295
- return "epic";
1296
- return "task";
1297
- }
1298
- function isPlainRecord2(candidate) {
1299
- return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1123
+ function safeReadJson(path) {
1124
+ try {
1125
+ return JSON.parse(readFileSync4(path, "utf-8"));
1126
+ } catch {
1127
+ return {};
1128
+ }
1300
1129
  }
1301
1130
 
1302
- // packages/runtime/src/control-plane/tasks/source-lifecycle.ts
1303
- function hasRunnableTaskSource(source) {
1304
- return Boolean(source && typeof source === "object" && !Array.isArray(source));
1131
+ // packages/runtime/src/control-plane/skill-materializer.ts
1132
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
1133
+ import { resolve as resolve8 } from "path";
1134
+ import { loadSkill } from "@rig/skill-loader";
1135
+ var MARKER_FILENAME = ".rig-plugin";
1136
+ function skillDirName(id) {
1137
+ return id.replace(/[^a-zA-Z0-9._-]+/g, "-");
1305
1138
  }
1306
- async function getPluginTask(projectRoot, taskId) {
1307
- const ctx = await buildPluginHostContext(projectRoot);
1308
- const [source] = ctx?.taskSourceRegistry.list() ?? [];
1309
- if (!hasRunnableTaskSource(source)) {
1310
- return ctx ? { configured: false, sourceKind: null, task: null } : null;
1139
+ async function materializeSkills(projectRoot, entries) {
1140
+ const skillsRoot = resolve8(projectRoot, ".pi", "skills");
1141
+ if (existsSync6(skillsRoot)) {
1142
+ for (const name of readdirSync(skillsRoot)) {
1143
+ const dir = resolve8(skillsRoot, name);
1144
+ if (existsSync6(resolve8(dir, MARKER_FILENAME))) {
1145
+ rmSync2(dir, { recursive: true, force: true });
1146
+ }
1147
+ }
1311
1148
  }
1312
- const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
1313
- return {
1314
- configured: true,
1315
- sourceKind: source.kind,
1316
- task
1317
- };
1318
- }
1319
- async function readConfiguredTaskSourceTask(projectRoot, taskId) {
1320
- const pluginResult = await getPluginTask(projectRoot, taskId);
1321
- if (pluginResult)
1322
- return pluginResult;
1323
- const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
1324
- return {
1325
- configured: false,
1326
- sourceKind: null,
1327
- task
1328
- };
1149
+ const written = [];
1150
+ for (const { pluginName, skill } of entries) {
1151
+ const sourcePath = resolve8(projectRoot, skill.path);
1152
+ if (!existsSync6(sourcePath)) {
1153
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${sourcePath} does not exist`);
1154
+ continue;
1155
+ }
1156
+ let body;
1157
+ try {
1158
+ await loadSkill(sourcePath);
1159
+ body = readFileSync5(sourcePath, "utf-8");
1160
+ } catch (err) {
1161
+ console.warn(`[plugin-host] skill "${skill.id}" from plugin "${pluginName}" not materialized: ${err instanceof Error ? err.message : err}`);
1162
+ continue;
1163
+ }
1164
+ const dir = resolve8(skillsRoot, skillDirName(skill.id));
1165
+ mkdirSync4(dir, { recursive: true });
1166
+ writeFileSync4(resolve8(dir, "SKILL.md"), body, "utf-8");
1167
+ writeFileSync4(resolve8(dir, MARKER_FILENAME), `${JSON.stringify({ plugin: pluginName, skillId: skill.id }, null, 2)}
1168
+ `, "utf-8");
1169
+ written.push({ id: skill.id, pluginName, directory: dir });
1170
+ }
1171
+ return written;
1329
1172
  }
1330
1173
 
1331
- // packages/runtime/src/control-plane/native/task-state.ts
1332
- import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
1333
- import { basename as basename6, resolve as resolve17 } from "path";
1334
-
1335
- // packages/runtime/src/control-plane/state-sync/types.ts
1336
- var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
1337
- var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
1338
- "draft",
1339
- "open",
1340
- "ready",
1341
- "queued",
1342
- "in_progress",
1343
- "under_review",
1344
- "blocked",
1345
- "completed",
1346
- "cancelled"
1347
- ]);
1348
- function normalizeTaskLifecycleStatus(status) {
1349
- switch (status) {
1350
- case "draft":
1351
- case "open":
1352
- case "ready":
1353
- case "queued":
1354
- case "in_progress":
1355
- case "under_review":
1356
- case "blocked":
1357
- case "completed":
1358
- case "cancelled":
1359
- return status;
1360
- case "closed":
1361
- return "completed";
1362
- case "running":
1363
- return "in_progress";
1364
- case "failed":
1365
- return "ready";
1366
- default:
1174
+ // packages/runtime/src/control-plane/plugin-host-context.ts
1175
+ async function buildPluginHostContext(projectRoot) {
1176
+ let config;
1177
+ try {
1178
+ config = await loadConfig(projectRoot);
1179
+ } catch (err) {
1180
+ const msg = err instanceof Error ? err.message : String(err);
1181
+ if (msg.includes("no rig.config")) {
1367
1182
  return null;
1183
+ }
1184
+ throw err;
1368
1185
  }
1369
- }
1370
- function normalizeTaskStateMetadataStatus(status) {
1371
- if (CANONICAL_TASK_LIFECYCLE_STATUSES.has(status)) {
1372
- return status;
1373
- }
1374
- switch (status) {
1375
- case "closed":
1376
- return "completed";
1377
- case "running":
1378
- return "in_progress";
1379
- case "failed":
1380
- return "ready";
1381
- default:
1382
- return;
1186
+ const pluginHost = createPluginHost(config.plugins);
1187
+ setScopeRules(config.workspace.scopeNormalization);
1188
+ const validatorRegistry = createValidatorRegistry();
1189
+ for (const impl of pluginHost.listExecutableValidators()) {
1190
+ validatorRegistry.register(impl);
1383
1191
  }
1384
- }
1385
- function canonicalizeTaskStateMetadata(raw) {
1386
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1387
- return null;
1192
+ const taskSourceRegistry = buildTaskSourceRegistry(config, pluginHost);
1193
+ const repoRegistry = createRepoRegistry(pluginHost.listRepoSources());
1194
+ const managedEntries = pluginHost.listRepoSources().map(repoRegistrationToManagedEntry).filter((e) => e !== null);
1195
+ setManagedRepos(managedEntries);
1196
+ const configRoleOverrides = config.runtime?.agentRoles;
1197
+ const agentRoleRegistry = createAgentRoleRegistry(pluginHost.listAgentRoles(), configRoleOverrides);
1198
+ const taskFieldRegistry = createTaskFieldRegistry(pluginHost.listTaskFieldExtensions());
1199
+ try {
1200
+ const hookEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.hooks ?? []).map((hook) => ({
1201
+ pluginName: plugin.name,
1202
+ hook
1203
+ })));
1204
+ if (hookEntries.length > 0) {
1205
+ materializeHooks(projectRoot, hookEntries);
1206
+ }
1207
+ } catch (err) {
1208
+ console.warn(`[plugin-host] hook materialization failed: ${err instanceof Error ? err.message : err}`);
1388
1209
  }
1389
- const metadata = raw;
1390
- const claimId = typeof metadata.claimId === "string" && metadata.claimId.length > 0 ? metadata.claimId : undefined;
1391
- const status = normalizeTaskStateMetadataStatus(metadata.status);
1392
- if (!status) {
1393
- return null;
1210
+ try {
1211
+ const skillEntries = config.plugins.flatMap((plugin) => (plugin.contributes?.skills ?? []).map((skill) => ({
1212
+ pluginName: plugin.name,
1213
+ skill
1214
+ })));
1215
+ if (skillEntries.length > 0) {
1216
+ await materializeSkills(projectRoot, skillEntries);
1217
+ }
1218
+ } catch (err) {
1219
+ console.warn(`[plugin-host] skill materialization failed: ${err instanceof Error ? err.message : err}`);
1394
1220
  }
1395
1221
  return {
1396
- ...claimId ? { claimId } : {},
1397
- status,
1398
- ...typeof metadata.ownerId === "string" && metadata.ownerId.length > 0 ? { ownerId: metadata.ownerId } : {},
1399
- ...typeof metadata.claimedAt === "string" && metadata.claimedAt.length > 0 ? { claimedAt: metadata.claimedAt } : {},
1400
- ...typeof metadata.lastEvidenceAt === "string" && metadata.lastEvidenceAt.length > 0 ? { lastEvidenceAt: metadata.lastEvidenceAt } : {},
1401
- ...typeof metadata.runId === "string" && metadata.runId.length > 0 ? { runId: metadata.runId } : {},
1402
- ...typeof metadata.branchName === "string" && metadata.branchName.length > 0 ? { branchName: metadata.branchName } : {},
1403
- ...typeof metadata.prNumber === "number" ? { prNumber: metadata.prNumber } : {},
1404
- ...typeof metadata.prUrl === "string" && metadata.prUrl.length > 0 ? { prUrl: metadata.prUrl } : {},
1405
- ...typeof metadata.reviewState === "string" && metadata.reviewState.length > 0 ? { reviewState: metadata.reviewState } : {},
1406
- ...typeof metadata.blockerReason === "string" && metadata.blockerReason.length > 0 ? { blockerReason: metadata.blockerReason } : {},
1407
- ...typeof metadata.sourceCommit === "string" && metadata.sourceCommit.length > 0 ? { sourceCommit: metadata.sourceCommit } : {}
1222
+ config,
1223
+ pluginHost,
1224
+ validatorRegistry,
1225
+ taskSourceRegistry,
1226
+ repoRegistry,
1227
+ agentRoleRegistry,
1228
+ taskFieldRegistry
1408
1229
  };
1409
1230
  }
1410
- function discardMismatchedTaskStateMetadata(input) {
1411
- input.taskId;
1412
- const canonicalMetadata = canonicalizeTaskStateMetadata(input.metadata);
1413
- if (!canonicalMetadata || !input.lifecycleStatus) {
1414
- return null;
1231
+
1232
+ // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
1233
+ import { spawnSync } from "child_process";
1234
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync2, statSync, writeFileSync as writeFileSync5 } from "fs";
1235
+ import { basename as basename3, join as join2, resolve as resolve10 } from "path";
1236
+
1237
+ // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
1238
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
1239
+ import { resolve as resolve9 } from "path";
1240
+
1241
+ // packages/runtime/src/control-plane/tasks/task-record-reader.ts
1242
+ async function findTaskById(reader, id) {
1243
+ const tasks = await reader.listTasks();
1244
+ return tasks.find((task) => task.id === id) ?? null;
1245
+ }
1246
+
1247
+ // packages/runtime/src/control-plane/tasks/legacy-task-config-source.ts
1248
+ class LegacyTaskConfigReadError extends Error {
1249
+ code = "LEGACY_TASK_CONFIG_READ_FAILED";
1250
+ projectRoot;
1251
+ configPath;
1252
+ cause;
1253
+ constructor(input) {
1254
+ super(input.message, { cause: input.cause });
1255
+ this.name = "LegacyTaskConfigReadError";
1256
+ this.projectRoot = input.projectRoot;
1257
+ this.configPath = input.configPath;
1258
+ this.cause = input.cause;
1415
1259
  }
1416
- const metadataStatus = canonicalMetadata.status ?? null;
1417
- if (metadataStatus && metadataStatus !== input.lifecycleStatus) {
1418
- return null;
1260
+ }
1261
+ function createLegacyTaskConfigRecordReader(projectRoot, options = {}) {
1262
+ const configPath = options.configPath ?? resolve9(projectRoot, ".rig", "task-config.json");
1263
+ const reader = {
1264
+ async listTasks() {
1265
+ return readLegacyTaskRecords(projectRoot, configPath);
1266
+ },
1267
+ async getTask(id) {
1268
+ return findTaskById(reader, id);
1269
+ }
1270
+ };
1271
+ return reader;
1272
+ }
1273
+ function readLegacyTaskRecords(projectRoot, configPath = resolve9(projectRoot, ".rig", "task-config.json")) {
1274
+ if (!existsSync7(configPath)) {
1275
+ return [];
1419
1276
  }
1420
- return canonicalMetadata;
1277
+ const rawConfig = readLegacyTaskConfigJson(projectRoot, configPath);
1278
+ return Object.entries(stripLegacyTaskConfigMetadata(rawConfig)).map(([id, entry]) => legacyTaskConfigEntryToRecord(id, entry)).filter((record) => record !== null);
1421
1279
  }
1422
- function readTaskStateMetadataEnvelope(raw) {
1423
- if (!raw || typeof raw !== "object") {
1424
- return { schemaVersion: SUPPORTED_TASK_STATE_SCHEMA_VERSION, supported: true, baseTrackerCommit: null, tasks: {} };
1280
+ function readLegacyTaskConfigJson(projectRoot, configPath) {
1281
+ try {
1282
+ const parsed = JSON.parse(readFileSync6(configPath, "utf8"));
1283
+ if (isPlainRecord(parsed)) {
1284
+ return parsed;
1285
+ }
1286
+ throw new Error("task config root must be a JSON object");
1287
+ } catch (cause) {
1288
+ throw new LegacyTaskConfigReadError({
1289
+ projectRoot,
1290
+ configPath,
1291
+ message: `Could not read legacy task config at ${configPath} for project ${projectRoot}: ${cause instanceof Error ? cause.message : String(cause)}`,
1292
+ cause
1293
+ });
1425
1294
  }
1426
- const envelope = raw;
1427
- const schemaVersion = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : SUPPORTED_TASK_STATE_SCHEMA_VERSION;
1428
- if (schemaVersion !== SUPPORTED_TASK_STATE_SCHEMA_VERSION) {
1429
- return { schemaVersion, supported: false, baseTrackerCommit: null, tasks: {} };
1295
+ }
1296
+ function stripLegacyTaskConfigMetadata(raw) {
1297
+ const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1298
+ return tasks;
1299
+ }
1300
+ function legacyTaskConfigEntryToRecord(id, entry) {
1301
+ if (!isPlainRecord(entry)) {
1302
+ return null;
1430
1303
  }
1431
- const rawTasks = envelope.tasks && typeof envelope.tasks === "object" && !Array.isArray(envelope.tasks) ? envelope.tasks : {};
1432
- const tasks = Object.fromEntries(Object.entries(rawTasks).map(([taskId, metadata]) => [taskId, canonicalizeTaskStateMetadata(metadata)]).filter((entry) => entry[1] != null));
1304
+ const deps = firstStringList(entry.deps, entry.dependencies, entry.validation_deps, entry.validationDeps);
1305
+ const validation = readStringList(entry.validation);
1306
+ const validators = readStringList(entry.validators);
1307
+ const scope = readStringList(entry.scope);
1308
+ const status = typeof entry.status === "string" ? entry.status : "open";
1309
+ const title = typeof entry.title === "string" ? entry.title : undefined;
1310
+ const description = typeof entry.description === "string" ? entry.description : undefined;
1311
+ const acceptanceCriteria = typeof entry.acceptance_criteria === "string" ? entry.acceptance_criteria : typeof entry.acceptanceCriteria === "string" ? entry.acceptanceCriteria : undefined;
1433
1312
  return {
1434
- schemaVersion,
1435
- supported: true,
1436
- baseTrackerCommit: typeof envelope.baseTrackerCommit === "string" && envelope.baseTrackerCommit.length > 0 ? envelope.baseTrackerCommit : null,
1437
- tasks
1313
+ id,
1314
+ deps,
1315
+ status,
1316
+ source: "legacy-task-config",
1317
+ ...title ? { title } : {},
1318
+ ...description ? { description } : {},
1319
+ ...acceptanceCriteria ? { acceptanceCriteria } : {},
1320
+ ...scope.length > 0 ? { scope } : {},
1321
+ ...validation.length > 0 ? { validation } : {},
1322
+ ...validators.length > 0 ? { validators } : {},
1323
+ ...preservedLegacyFields(entry)
1438
1324
  };
1439
1325
  }
1440
- // packages/runtime/src/control-plane/state-sync/read.ts
1441
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
1442
- import { resolve as resolve16 } from "path";
1443
-
1444
- // packages/runtime/src/control-plane/native/git-native.ts
1445
- import { chmodSync, copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
1446
- import { tmpdir as tmpdir3 } from "os";
1447
- import { dirname as dirname5, isAbsolute, resolve as resolve10 } from "path";
1448
- import { createHash } from "crypto";
1449
- function isTextTreeCommitUpdate(update) {
1450
- return typeof update.content === "string";
1451
- }
1452
- var sharedGitNativeOutputDir = resolve10(tmpdir3(), "rig-native");
1453
- var sharedGitNativeOutputPath = resolve10(sharedGitNativeOutputDir, `rig-git-${process.platform}-${process.arch}${process.platform === "win32" ? ".exe" : ""}`);
1454
- var trackerCommandUsageProbe = "usage: rig-git fetch-ref <repo-path> <remote> <branch>";
1455
- function temporaryGitBinaryOutputPath(outputPath) {
1456
- const suffix = process.platform === "win32" ? ".exe" : "";
1457
- return resolve10(dirname5(outputPath), `.rig-git-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}${suffix}`);
1458
- }
1459
- function publishGitBinary(tempOutputPath, outputPath) {
1460
- try {
1461
- renameSync(tempOutputPath, outputPath);
1462
- } catch (error) {
1463
- if (process.platform === "win32" && existsSync8(outputPath)) {
1464
- rmSync2(outputPath, { force: true });
1465
- renameSync(tempOutputPath, outputPath);
1466
- return;
1326
+ function preservedLegacyFields(entry) {
1327
+ const preserved = {};
1328
+ for (const key of [
1329
+ "role",
1330
+ "browser",
1331
+ "repo_pins",
1332
+ "criticality",
1333
+ "queue_weight",
1334
+ "creates_repo",
1335
+ "auto_synced"
1336
+ ]) {
1337
+ if (entry[key] !== undefined) {
1338
+ preserved[key] = entry[key];
1467
1339
  }
1468
- throw error;
1469
1340
  }
1341
+ return preserved;
1470
1342
  }
1471
- function runtimeRigGitFileName() {
1472
- return `rig-git${process.platform === "win32" ? ".exe" : ""}`;
1473
- }
1474
- function rigGitSourceCandidates() {
1475
- const execDir = process.execPath?.trim() ? dirname5(process.execPath.trim()) : "";
1476
- const cwd = process.cwd()?.trim() || "";
1477
- const projectRoot = process.env.PROJECT_RIG_ROOT?.trim() || "";
1478
- const hostProjectRoot = process.env.RIG_HOST_PROJECT_ROOT?.trim() || "";
1479
- const moduleRelativeSource = resolve10(import.meta.dir, "../../../native/rig-git.zig");
1480
- return [...new Set([
1481
- process.env.RIG_NATIVE_GIT_SOURCE?.trim() || "",
1482
- moduleRelativeSource,
1483
- projectRoot ? resolve10(projectRoot, "packages/runtime/native/rig-git.zig") : "",
1484
- hostProjectRoot ? resolve10(hostProjectRoot, "packages/runtime/native/rig-git.zig") : "",
1485
- cwd ? resolve10(cwd, "packages/runtime/native/rig-git.zig") : "",
1486
- execDir ? resolve10(execDir, "..", "..", "packages/runtime/native/rig-git.zig") : "",
1487
- execDir ? resolve10(execDir, "..", "native", "rig-git.zig") : ""
1488
- ].filter(Boolean))];
1343
+ function firstStringList(...candidates) {
1344
+ for (const candidate of candidates) {
1345
+ const list = readStringList(candidate);
1346
+ if (list.length > 0) {
1347
+ return list;
1348
+ }
1349
+ }
1350
+ return [];
1489
1351
  }
1490
- function nativePackageBinaryCandidates(fromDir, fileName) {
1491
- const candidates = [];
1492
- let cursor = resolve10(fromDir);
1493
- for (let index = 0;index < 8; index += 1) {
1494
- candidates.push(resolve10(cursor, "native", `${process.platform}-${process.arch}`, fileName), resolve10(cursor, "native", `${process.platform}-${process.arch}`, "bin", fileName), resolve10(cursor, "native", fileName), resolve10(cursor, "native", "bin", fileName));
1495
- const parent = dirname5(cursor);
1496
- if (parent === cursor)
1497
- break;
1498
- cursor = parent;
1352
+ function readStringList(candidate) {
1353
+ if (!Array.isArray(candidate)) {
1354
+ return [];
1499
1355
  }
1500
- return candidates;
1356
+ return candidate.filter((value) => typeof value === "string");
1501
1357
  }
1502
- function rigGitBinaryCandidates() {
1503
- const execDir = process.execPath?.trim() ? dirname5(process.execPath.trim()) : "";
1504
- const fileName = runtimeRigGitFileName();
1505
- const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1506
- return [...new Set([
1507
- explicit,
1508
- ...nativePackageBinaryCandidates(import.meta.dir, fileName),
1509
- execDir ? resolve10(execDir, fileName) : "",
1510
- execDir ? resolve10(execDir, "..", fileName) : "",
1511
- execDir ? resolve10(execDir, "..", "bin", fileName) : "",
1512
- sharedGitNativeOutputPath
1513
- ].filter(Boolean))];
1358
+ function isPlainRecord(candidate) {
1359
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1514
1360
  }
1515
- function resolveGitSourcePath() {
1516
- for (const candidate of rigGitSourceCandidates()) {
1517
- if (candidate && existsSync8(candidate)) {
1518
- return candidate;
1361
+
1362
+ // packages/runtime/src/control-plane/tasks/source-aware-task-config-source.ts
1363
+ var STATUS_LABELS = new Set(["ready", "blocked", "in-progress", "under-review", "failed", "cancelled"]);
1364
+ var FILE_TASK_PATTERN = /\.(task\.)?json$/;
1365
+ function createSourceAwareTaskConfigRecordReader(projectRoot, options = {}) {
1366
+ const configPath = options.configPath ?? resolve10(projectRoot, ".rig", "task-config.json");
1367
+ const legacy = createLegacyTaskConfigRecordReader(projectRoot, { configPath });
1368
+ const spawnFn = options.spawn ?? spawnSync;
1369
+ const ghBinary = options.ghBinary ?? "gh";
1370
+ const allowLocalFallback = options.allowLocalTaskConfigStatusFallback ?? true;
1371
+ return {
1372
+ async listTasks() {
1373
+ const rawConfig = readRawTaskConfig(configPath);
1374
+ if (!rawConfig) {
1375
+ const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1376
+ return configuredFilesPath ? listFileBackedTasks(projectRoot, configuredFilesPath) : [];
1377
+ }
1378
+ const tasks = [];
1379
+ const legacyTasks = await legacy.listTasks();
1380
+ const legacyById = new Map(legacyTasks.map((task) => [task.id, task]));
1381
+ for (const [id, rawEntry] of Object.entries(stripLegacyTaskConfigMetadata2(rawConfig))) {
1382
+ if (!isPlainRecord2(rawEntry)) {
1383
+ continue;
1384
+ }
1385
+ const metadata = readMaterializedTaskMetadata(rawEntry);
1386
+ if (metadata.taskSource?.kind === "github-issues") {
1387
+ tasks.push(readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry));
1388
+ continue;
1389
+ }
1390
+ if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1391
+ const fileTask = readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1392
+ if (fileTask)
1393
+ tasks.push(fileTask);
1394
+ continue;
1395
+ }
1396
+ if (!allowLocalFallback) {
1397
+ continue;
1398
+ }
1399
+ const legacyTask = legacyById.get(id);
1400
+ if (legacyTask) {
1401
+ tasks.push(legacyTask);
1402
+ }
1403
+ }
1404
+ return tasks;
1405
+ },
1406
+ async getTask(id) {
1407
+ const rawEntry = readRawTaskEntry(configPath, id);
1408
+ if (!rawEntry) {
1409
+ const configuredFilesPath = readConfiguredFilesTaskSourcePath(projectRoot);
1410
+ return configuredFilesPath ? readFileBackedTask(projectRoot, configuredFilesPath, id, {}) : null;
1411
+ }
1412
+ const metadata = readMaterializedTaskMetadata(rawEntry);
1413
+ if (metadata.taskSource?.kind === "github-issues") {
1414
+ return readGithubIssueTask(ghBinary, spawnFn, id, metadata, rawEntry);
1415
+ }
1416
+ if (metadata.taskSource?.kind === "files" && metadata.taskSource.path) {
1417
+ return readFileBackedTask(projectRoot, metadata.taskSource.path, id, rawEntry);
1418
+ }
1419
+ return allowLocalFallback ? legacy.getTask(id) : null;
1519
1420
  }
1520
- }
1521
- return null;
1421
+ };
1522
1422
  }
1523
- function resolveGitBinaryPath() {
1524
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1525
- return null;
1423
+ function readMaterializedTaskMetadata(entry) {
1424
+ const rawRig = entry._rig;
1425
+ if (!isPlainRecord2(rawRig)) {
1426
+ return {};
1526
1427
  }
1527
- for (const candidate of rigGitBinaryCandidates()) {
1528
- if (candidate && existsSync8(candidate)) {
1529
- return candidate;
1428
+ const rawSource = rawRig.taskSource;
1429
+ const metadata = {};
1430
+ if (isPlainRecord2(rawSource)) {
1431
+ const kind = typeof rawSource.kind === "string" ? rawSource.kind : "";
1432
+ if (kind.length > 0) {
1433
+ metadata.taskSource = {
1434
+ kind,
1435
+ ...typeof rawSource.path === "string" ? { path: rawSource.path } : {},
1436
+ ...typeof rawSource.owner === "string" ? { owner: rawSource.owner } : {},
1437
+ ...typeof rawSource.repo === "string" ? { repo: rawSource.repo } : {},
1438
+ ...Array.isArray(rawSource.labels) ? { labels: rawSource.labels.filter((label) => typeof label === "string") } : {},
1439
+ ...rawSource.state === "open" || rawSource.state === "closed" || rawSource.state === "all" ? { state: rawSource.state } : {}
1440
+ };
1530
1441
  }
1531
1442
  }
1532
- return null;
1533
- }
1534
- function preferredGitBinaryOutputPath() {
1535
- const explicit = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1536
- return explicit || sharedGitNativeOutputPath;
1443
+ if (typeof rawRig.sourceIssueId === "string") {
1444
+ metadata.sourceIssueId = rawRig.sourceIssueId;
1445
+ }
1446
+ return metadata;
1537
1447
  }
1538
- function binarySupportsTrackerCommandsSync(binaryPath) {
1539
- try {
1540
- const probe = Bun.spawnSync([binaryPath, "fetch-ref", "."], {
1541
- stdout: "pipe",
1542
- stderr: "pipe"
1543
- });
1544
- const stdout = probe.stdout.toString().trim();
1545
- const stderr = probe.stderr.toString().trim();
1546
- if (stdout.includes('"error":"unknown command"')) {
1547
- return false;
1448
+ function readConfiguredFilesTaskSourcePath(projectRoot) {
1449
+ const jsonPath = resolve10(projectRoot, "rig.config.json");
1450
+ if (existsSync8(jsonPath)) {
1451
+ try {
1452
+ const parsed = JSON.parse(readFileSync7(jsonPath, "utf8"));
1453
+ if (isPlainRecord2(parsed) && isPlainRecord2(parsed.taskSource)) {
1454
+ const source = parsed.taskSource;
1455
+ return source.kind === "files" && typeof source.path === "string" ? source.path : null;
1456
+ }
1457
+ } catch {
1458
+ return null;
1548
1459
  }
1549
- return probe.exitCode === 2 && stderr.includes(trackerCommandUsageProbe);
1550
- } catch {
1551
- return false;
1552
1460
  }
1553
- }
1554
- function nativeBuildManifestPath(outputPath) {
1555
- return `${outputPath}.build-manifest.json`;
1556
- }
1557
- function hasMatchingNativeBuildManifestSync(manifestPath, buildKey) {
1558
- if (!existsSync8(manifestPath)) {
1559
- return false;
1461
+ const tsPath = resolve10(projectRoot, "rig.config.ts");
1462
+ if (!existsSync8(tsPath)) {
1463
+ return null;
1560
1464
  }
1561
1465
  try {
1562
- const manifest = JSON.parse(readFileSync7(manifestPath, "utf8"));
1563
- return manifest.version === 1 && manifest.buildKey === buildKey;
1466
+ const source = readFileSync7(tsPath, "utf8");
1467
+ const taskSourceBlock = source.match(/taskSource\s*:\s*\{[\s\S]*?\}/m)?.[0] ?? "";
1468
+ const kind = taskSourceBlock.match(/kind\s*:\s*["']([^"']+)["']/)?.[1];
1469
+ if (kind !== "files") {
1470
+ return null;
1471
+ }
1472
+ return taskSourceBlock.match(/path\s*:\s*["']([^"']+)["']/)?.[1] ?? null;
1564
1473
  } catch {
1565
- return false;
1474
+ return null;
1566
1475
  }
1567
1476
  }
1568
- function sha256FileSync(path) {
1569
- return createHash("sha256").update(readFileSync7(path)).digest("hex");
1570
- }
1571
- function ensureRigGitBinaryPathSync(outputPath = preferredGitBinaryOutputPath()) {
1572
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1573
- throw new Error("Zig native git is disabled via RIG_DISABLE_ZIG_NATIVE=1");
1574
- }
1575
- const sourcePath = resolveGitSourcePath();
1576
- if (!sourcePath) {
1577
- const binaryPath = resolveGitBinaryPath();
1578
- if (binaryPath) {
1579
- return binaryPath;
1580
- }
1581
- throw new Error("rig-git.zig source file not found.");
1477
+ function readRawTaskEntry(configPath, taskId) {
1478
+ const rawConfig = readRawTaskConfig(configPath);
1479
+ if (!rawConfig) {
1480
+ return null;
1582
1481
  }
1583
- const zigBinary = Bun.which("zig");
1584
- if (!zigBinary) {
1585
- throw new Error("zig is required to build native Rig git tools.");
1482
+ const entry = stripLegacyTaskConfigMetadata2(rawConfig)[taskId];
1483
+ return isPlainRecord2(entry) ? entry : null;
1484
+ }
1485
+ function readRawTaskConfig(configPath) {
1486
+ if (!existsSync8(configPath)) {
1487
+ return null;
1586
1488
  }
1587
- mkdirSync4(dirname5(outputPath), { recursive: true });
1588
- const sourceDigest = sha256FileSync(sourcePath);
1589
- const buildKey = JSON.stringify({
1590
- version: 1,
1591
- zigBinary,
1592
- platform: process.platform,
1593
- arch: process.arch,
1594
- sourcePath,
1595
- sourceDigest
1596
- });
1597
- const manifestPath = nativeBuildManifestPath(outputPath);
1598
- const needsBuild = !existsSync8(outputPath) || !hasMatchingNativeBuildManifestSync(manifestPath, buildKey) || !binarySupportsTrackerCommandsSync(outputPath);
1599
- if (!needsBuild) {
1600
- chmodSync(outputPath, 493);
1601
- return outputPath;
1489
+ const parsed = JSON.parse(readFileSync7(configPath, "utf8"));
1490
+ return isPlainRecord2(parsed) ? parsed : null;
1491
+ }
1492
+ function stripLegacyTaskConfigMetadata2(raw) {
1493
+ const { validation_descriptions: _legacyDescriptions, _meta, ...tasks } = raw;
1494
+ return tasks;
1495
+ }
1496
+ function listFileBackedTasks(projectRoot, sourcePath) {
1497
+ const directory = resolve10(projectRoot, sourcePath);
1498
+ if (!existsSync8(directory)) {
1499
+ return [];
1602
1500
  }
1603
- const tempOutputPath = temporaryGitBinaryOutputPath(outputPath);
1604
- const build = Bun.spawnSync([
1605
- zigBinary,
1606
- "build-exe",
1607
- sourcePath,
1608
- "-O",
1609
- "ReleaseFast",
1610
- `-femit-bin=${tempOutputPath}`
1611
- ], {
1612
- cwd: dirname5(sourcePath),
1613
- stdout: "pipe",
1614
- stderr: "pipe"
1615
- });
1616
- if (build.exitCode !== 0 || !existsSync8(tempOutputPath)) {
1617
- const stderr = build.stderr.toString().trim();
1618
- const stdout = build.stdout.toString().trim();
1619
- const details = [stderr, stdout].filter(Boolean).join(`
1620
- `);
1621
- throw new Error(`Failed to build native Rig git tools: ${details || `zig exited with code ${build.exitCode}`}`);
1501
+ const tasks = [];
1502
+ for (const name of readdirSync2(directory)) {
1503
+ if (!FILE_TASK_PATTERN.test(name))
1504
+ continue;
1505
+ const inferredId = basename3(name).replace(FILE_TASK_PATTERN, "");
1506
+ const task = readFileBackedTask(projectRoot, sourcePath, inferredId, {});
1507
+ if (task)
1508
+ tasks.push(task);
1622
1509
  }
1623
- chmodSync(tempOutputPath, 493);
1624
- if (existsSync8(outputPath) && hasMatchingNativeBuildManifestSync(manifestPath, buildKey)) {
1625
- rmSync2(tempOutputPath, { force: true });
1626
- chmodSync(outputPath, 493);
1627
- return outputPath;
1510
+ return tasks;
1511
+ }
1512
+ function readFileBackedTask(projectRoot, sourcePath, taskId, rawEntry) {
1513
+ const file = findFileBackedTaskFile(resolve10(projectRoot, sourcePath), taskId);
1514
+ if (!file) {
1515
+ return null;
1628
1516
  }
1629
- publishGitBinary(tempOutputPath, outputPath);
1630
- if (!binarySupportsTrackerCommandsSync(outputPath)) {
1631
- rmSync2(outputPath, { force: true });
1632
- throw new Error("Failed to build native Rig git tools: tracker command probe failed");
1517
+ const raw = JSON.parse(readFileSync7(file, "utf8"));
1518
+ if (!isPlainRecord2(raw)) {
1519
+ return null;
1633
1520
  }
1634
- writeFileSync5(manifestPath, `${JSON.stringify({ version: 1, buildKey }, null, 2)}
1635
- `, "utf8");
1636
- return outputPath;
1521
+ return {
1522
+ id: typeof raw.id === "string" ? raw.id : taskId,
1523
+ deps: Array.isArray(raw.deps) ? raw.deps : Array.isArray(raw.depends_on) ? raw.depends_on : [],
1524
+ status: typeof raw.status === "string" ? raw.status : "ready",
1525
+ title: typeof raw.title === "string" ? raw.title : typeof rawEntry.title === "string" ? rawEntry.title : taskId,
1526
+ ...raw
1527
+ };
1637
1528
  }
1638
- function runGitNative(command, args) {
1639
- if (process.env.RIG_DISABLE_ZIG_NATIVE === "1") {
1640
- return { ok: false, error: "rig-git native disabled" };
1529
+ function findFileBackedTaskFile(directory, taskId) {
1530
+ if (!existsSync8(directory)) {
1531
+ return null;
1641
1532
  }
1642
- const trackerCommand = command === "fetch-ref" || command === "read-blob-at-ref" || command === "write-tree-commit" || command === "push-ref-with-lease";
1643
- let binaryPath = null;
1644
- if (trackerCommand) {
1533
+ for (const name of readdirSync2(directory)) {
1534
+ if (!FILE_TASK_PATTERN.test(name))
1535
+ continue;
1536
+ const file = join2(directory, name);
1645
1537
  try {
1646
- binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
1647
- } catch (error) {
1648
- const message = error instanceof Error ? error.message : String(error);
1649
- if (message.includes("rig-git.zig source file not found")) {
1650
- return { ok: false, error: "rig-git binary not found" };
1651
- }
1652
- return { ok: false, error: message };
1653
- }
1654
- } else {
1655
- const explicitBinaryPath = process.env.RIG_NATIVE_GIT_BIN?.trim() || "";
1656
- binaryPath = explicitBinaryPath && existsSync8(explicitBinaryPath) ? explicitBinaryPath : !explicitBinaryPath ? resolveGitBinaryPath() : null;
1657
- if (!binaryPath) {
1658
- try {
1659
- binaryPath = ensureRigGitBinaryPathSync(preferredGitBinaryOutputPath());
1660
- } catch (error) {
1661
- const message = error instanceof Error ? error.message : String(error);
1662
- if (message.includes("rig-git.zig source file not found")) {
1663
- return { ok: false, error: "rig-git binary not found" };
1664
- }
1665
- return { ok: false, error: message };
1538
+ if (!statSync(file).isFile())
1539
+ continue;
1540
+ const raw = JSON.parse(readFileSync7(file, "utf8"));
1541
+ const inferredId = basename3(file).replace(FILE_TASK_PATTERN, "");
1542
+ const id = isPlainRecord2(raw) && typeof raw.id === "string" ? raw.id : inferredId;
1543
+ if (id === taskId) {
1544
+ return file;
1666
1545
  }
1667
- }
1546
+ } catch {}
1668
1547
  }
1669
- try {
1670
- const proc = Bun.spawnSync([binaryPath, command, ...args], {
1671
- stdout: "pipe",
1672
- stderr: "pipe",
1673
- env: process.env
1674
- });
1675
- if (proc.exitCode !== 0) {
1676
- const stdoutText = proc.stdout.toString().trim();
1677
- if (stdoutText) {
1678
- try {
1679
- const parsed = JSON.parse(stdoutText);
1680
- if (!parsed.ok) {
1681
- return parsed;
1682
- }
1683
- } catch {}
1684
- }
1685
- const errText = proc.stderr.toString().trim() || `exit code ${proc.exitCode}`;
1686
- return { ok: false, error: errText };
1687
- }
1688
- const output = proc.stdout.toString().trim();
1689
- return JSON.parse(output);
1690
- } catch (err) {
1691
- return { ok: false, error: String(err) };
1548
+ return null;
1549
+ }
1550
+ function readGithubIssueTask(bin, spawnFn, id, metadata, rawEntry) {
1551
+ const source = requireGithubIssueSource(metadata, id);
1552
+ const issue = runGh(bin, [
1553
+ "issue",
1554
+ "view",
1555
+ String(id),
1556
+ "--repo",
1557
+ `${source.owner}/${source.repo}`,
1558
+ "--json",
1559
+ "number,title,body,labels,state,url,assignees"
1560
+ ], spawnFn);
1561
+ return githubIssueToTask(issue, source, rawEntry);
1562
+ }
1563
+ function requireGithubIssueSource(metadata, id) {
1564
+ const source = metadata.taskSource;
1565
+ if (source?.kind === "github-issues" && source.owner && source.repo) {
1566
+ return { owner: source.owner, repo: source.repo };
1567
+ }
1568
+ const parsed = metadata.sourceIssueId?.match(/^([^/]+)\/([^#]+)#(\d+)$/);
1569
+ if (parsed && parsed[3] === id) {
1570
+ return { owner: parsed[1], repo: parsed[2] };
1692
1571
  }
1572
+ throw new Error(`Task ${id} is marked as github-issues but has no owner/repo source metadata`);
1693
1573
  }
1694
- function requireGitNative(command, args) {
1695
- const result = runGitNative(command, args);
1696
- if (!result.ok) {
1697
- throw new Error(`rig-git ${command} failed: ${result.error}`);
1574
+ function githubIssueToTask(issue, source, rawEntry) {
1575
+ const labelNames = (issue.labels ?? []).map((label) => label.name);
1576
+ const scope = labelNames.filter((label) => label.startsWith("scope:")).map((label) => label.slice("scope:".length));
1577
+ const roleLabel = labelNames.find((label) => label.startsWith("role:"));
1578
+ const validators = labelNames.filter((label) => label.startsWith("validator:")).map((label) => label.slice("validator:".length));
1579
+ const body = issue.body ?? "";
1580
+ const repo = `${source.owner}/${source.repo}`;
1581
+ return {
1582
+ id: String(issue.number),
1583
+ deps: parseDeps(body),
1584
+ status: githubStatusFor(issue),
1585
+ title: issue.title,
1586
+ body,
1587
+ ...scope.length > 0 ? { scope } : {},
1588
+ ...roleLabel ? { role: roleLabel.slice("role:".length) } : typeof rawEntry.role === "string" ? { role: rawEntry.role } : {},
1589
+ ...validators.length > 0 ? { validators } : {},
1590
+ ...issue.url ? { url: issue.url } : {},
1591
+ issueType: issueTypeFor(labelNames),
1592
+ sourceIssueId: `${repo}#${issue.number}`,
1593
+ parentChildDeps: parseParents(body),
1594
+ labels: labelNames,
1595
+ raw: issue,
1596
+ source: "github-issues",
1597
+ _rig: {
1598
+ taskSource: { kind: "github-issues", owner: source.owner, repo: source.repo },
1599
+ sourceIssueId: `${repo}#${issue.number}`
1600
+ }
1601
+ };
1602
+ }
1603
+ function githubStatusFor(issue) {
1604
+ const state = (issue.state ?? "").toUpperCase();
1605
+ if (state === "CLOSED")
1606
+ return "closed";
1607
+ const labelNames = (issue.labels ?? []).map((label) => label.name);
1608
+ if (labelNames.includes("in-progress"))
1609
+ return "in_progress";
1610
+ if (labelNames.includes("blocked"))
1611
+ return "blocked";
1612
+ if (labelNames.includes("ready"))
1613
+ return "ready";
1614
+ if (labelNames.includes("under-review"))
1615
+ return "under_review";
1616
+ if (labelNames.includes("failed"))
1617
+ return "failed";
1618
+ if (labelNames.includes("cancelled"))
1619
+ return "cancelled";
1620
+ return "open";
1621
+ }
1622
+ function selectedGitHubEnv() {
1623
+ const token = process.env.RIG_GITHUB_SELECTED_TOKEN?.trim() || process.env.RIG_GITHUB_TOKEN?.trim() || "";
1624
+ return { GH_TOKEN: token, GITHUB_TOKEN: token, RIG_GITHUB_TOKEN: token };
1625
+ }
1626
+ function ghSpawnOptions() {
1627
+ return { encoding: "utf-8", env: { ...process.env, ...selectedGitHubEnv() } };
1628
+ }
1629
+ function runGh(bin, args, spawnFn) {
1630
+ const res = spawnFn(bin, [...args], ghSpawnOptions());
1631
+ assertGhSuccess(args, res);
1632
+ if (!res.stdout || res.stdout.trim() === "") {
1633
+ throw new Error(`gh ${args.join(" ")} returned empty stdout`);
1698
1634
  }
1699
- return result;
1635
+ return JSON.parse(res.stdout);
1700
1636
  }
1701
- function requireGitNativeString(command, args) {
1702
- const result = requireGitNative(command, args);
1703
- if ("value" in result && typeof result.value === "string") {
1704
- return result.value;
1637
+ function assertGhSuccess(args, res) {
1638
+ if (res.error) {
1639
+ const msg = res.error.message ?? String(res.error);
1640
+ throw new Error(`gh CLI not available \u2014 install gh (brew install gh / apt install gh): ${msg}`);
1641
+ }
1642
+ if (res.status !== 0) {
1643
+ throw new Error(`gh ${args.join(" ")} failed (exit ${res.status}): ${res.stderr}`);
1705
1644
  }
1706
- throw new Error(`rig-git ${command} returned an unexpected result payload`);
1707
1645
  }
1708
- function nativeBranchName(repoPath) {
1709
- const result = runGitNative("branch-name", [repoPath]);
1710
- if (!result.ok)
1711
- return null;
1712
- if ("value" in result && typeof result.value === "string")
1713
- return result.value;
1714
- return null;
1646
+ function parseDeps(body) {
1647
+ return parseIssueRefs(body, /^depends-on:\s*([^\n]+)/im);
1715
1648
  }
1716
- function nativeHeadOid(repoPath) {
1717
- const result = runGitNative("head-oid", [repoPath]);
1718
- if (!result.ok)
1719
- return null;
1720
- if ("value" in result && typeof result.value === "string")
1721
- return result.value;
1722
- return null;
1649
+ function parseParents(body) {
1650
+ return parseIssueRefs(body, /^parents?:\s*([^\n]+)/im);
1723
1651
  }
1724
- function nativeChangeCount(repoPath) {
1725
- const result = runGitNative("change-count", [repoPath]);
1726
- if (!result.ok)
1727
- return null;
1728
- if ("count" in result && typeof result.count === "number")
1729
- return result.count;
1730
- return null;
1652
+ function parseIssueRefs(body, pattern) {
1653
+ const match = body.match(pattern);
1654
+ if (!match)
1655
+ return [];
1656
+ return match[1].split(",").map((value) => value.trim()).map((value) => value.replace(/^#/, "").match(/^(\d+)/)?.[1] ?? "").filter((value) => value.length > 0);
1731
1657
  }
1732
- function nativePendingFiles(repoPath) {
1733
- const result = runGitNative("pending-files", [repoPath]);
1734
- if (!result.ok)
1735
- return null;
1736
- if ("files" in result && Array.isArray(result.files)) {
1737
- return result.files.map((f) => ({ path: f.path, status: f.status }));
1738
- }
1739
- return null;
1658
+ function issueTypeFor(labels) {
1659
+ const typed = labels.find((label) => label.startsWith("type:"));
1660
+ if (typed)
1661
+ return typed.slice("type:".length);
1662
+ if (labels.includes("epic"))
1663
+ return "epic";
1664
+ return "task";
1740
1665
  }
1741
- function nativeFileHasChanges(repoPath, filePath) {
1742
- const result = runGitNative("file-has-changes", [repoPath, filePath]);
1743
- if (!result.ok)
1744
- return null;
1745
- if ("has_changes" in result && typeof result.has_changes === "boolean")
1746
- return result.has_changes;
1747
- return null;
1666
+ function isPlainRecord2(candidate) {
1667
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1748
1668
  }
1749
- function nativeFetchRef(repoPath, remote, branch) {
1750
- return requireGitNativeString("fetch-ref", [repoPath, remote, branch]);
1669
+
1670
+ // packages/runtime/src/control-plane/tasks/source-lifecycle.ts
1671
+ function hasRunnableTaskSource(source) {
1672
+ return Boolean(source && typeof source === "object" && !Array.isArray(source));
1751
1673
  }
1752
- function nativeReadBlobAtRef(repoPath, ref, path) {
1753
- const requestDir = resolve10(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
1754
- mkdirSync4(requestDir, { recursive: true });
1755
- const outputPath = resolve10(requestDir, "blob.txt");
1756
- try {
1757
- requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
1758
- return readFileSync7(outputPath, "utf8");
1759
- } finally {
1760
- rmSync2(requestDir, { recursive: true, force: true });
1674
+ async function getPluginTask(projectRoot, taskId) {
1675
+ const ctx = await buildPluginHostContext(projectRoot);
1676
+ const [source] = ctx?.taskSourceRegistry.list() ?? [];
1677
+ if (!hasRunnableTaskSource(source)) {
1678
+ return ctx ? { configured: false, sourceKind: null, task: null } : null;
1761
1679
  }
1680
+ const task = source.get ? await source.get(taskId) ?? null : (await source.list()).find((entry) => entry.id === taskId) ?? null;
1681
+ return {
1682
+ configured: true,
1683
+ sourceKind: source.kind,
1684
+ task
1685
+ };
1762
1686
  }
1763
- function nativeReadBlobBytesAtRef(repoPath, ref, path) {
1764
- const requestDir = resolve10(sharedGitNativeOutputDir, "reads", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
1765
- mkdirSync4(requestDir, { recursive: true });
1766
- const outputPath = resolve10(requestDir, "blob.bin");
1767
- try {
1768
- requireGitNative("read-blob-at-ref", [repoPath, ref, path, outputPath]);
1769
- return readFileSync7(outputPath);
1770
- } finally {
1771
- rmSync2(requestDir, { recursive: true, force: true });
1687
+ async function readConfiguredTaskSourceTask(projectRoot, taskId) {
1688
+ const pluginResult = await getPluginTask(projectRoot, taskId);
1689
+ if (pluginResult)
1690
+ return pluginResult;
1691
+ const task = await createSourceAwareTaskConfigRecordReader(projectRoot).getTask(taskId);
1692
+ return {
1693
+ configured: false,
1694
+ sourceKind: null,
1695
+ task
1696
+ };
1697
+ }
1698
+
1699
+ // packages/runtime/src/control-plane/native/task-state.ts
1700
+ import { existsSync as existsSync15, readFileSync as readFileSync10, readdirSync as readdirSync3, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
1701
+ import { basename as basename6, resolve as resolve17 } from "path";
1702
+
1703
+ // packages/runtime/src/control-plane/state-sync/types.ts
1704
+ var SUPPORTED_TASK_STATE_SCHEMA_VERSION = 1;
1705
+ var CANONICAL_TASK_LIFECYCLE_STATUSES = new Set([
1706
+ "draft",
1707
+ "open",
1708
+ "ready",
1709
+ "queued",
1710
+ "in_progress",
1711
+ "under_review",
1712
+ "blocked",
1713
+ "completed",
1714
+ "cancelled"
1715
+ ]);
1716
+ function normalizeTaskLifecycleStatus(status) {
1717
+ switch (status) {
1718
+ case "draft":
1719
+ case "open":
1720
+ case "ready":
1721
+ case "queued":
1722
+ case "in_progress":
1723
+ case "under_review":
1724
+ case "blocked":
1725
+ case "completed":
1726
+ case "cancelled":
1727
+ return status;
1728
+ case "closed":
1729
+ return "completed";
1730
+ case "running":
1731
+ return "in_progress";
1732
+ case "failed":
1733
+ return "ready";
1734
+ default:
1735
+ return null;
1772
1736
  }
1773
1737
  }
1774
- function serializeTreeCommitUpdates(updates) {
1775
- return updates.map((update) => {
1776
- if (isTextTreeCommitUpdate(update)) {
1777
- return { path: update.path, kind: "text", content: update.content };
1778
- }
1779
- if (!isAbsolute(update.sourceFilePath)) {
1780
- throw new Error("tree commit binary updates require an absolute sourceFilePath");
1781
- }
1782
- return { path: update.path, kind: "file", sourceFilePath: update.sourceFilePath };
1783
- });
1738
+ function normalizeTaskStateMetadataStatus(status) {
1739
+ if (CANONICAL_TASK_LIFECYCLE_STATUSES.has(status)) {
1740
+ return status;
1741
+ }
1742
+ switch (status) {
1743
+ case "closed":
1744
+ return "completed";
1745
+ case "running":
1746
+ return "in_progress";
1747
+ case "failed":
1748
+ return "ready";
1749
+ default:
1750
+ return;
1751
+ }
1784
1752
  }
1785
- function buildTreeCommitUpdatesJson(updates) {
1786
- return `${JSON.stringify(serializeTreeCommitUpdates(updates), null, 2)}
1787
- `;
1753
+ function canonicalizeTaskStateMetadata(raw) {
1754
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1755
+ return null;
1756
+ }
1757
+ const metadata = raw;
1758
+ const claimId = typeof metadata.claimId === "string" && metadata.claimId.length > 0 ? metadata.claimId : undefined;
1759
+ const status = normalizeTaskStateMetadataStatus(metadata.status);
1760
+ if (!status) {
1761
+ return null;
1762
+ }
1763
+ return {
1764
+ ...claimId ? { claimId } : {},
1765
+ status,
1766
+ ...typeof metadata.ownerId === "string" && metadata.ownerId.length > 0 ? { ownerId: metadata.ownerId } : {},
1767
+ ...typeof metadata.claimedAt === "string" && metadata.claimedAt.length > 0 ? { claimedAt: metadata.claimedAt } : {},
1768
+ ...typeof metadata.lastEvidenceAt === "string" && metadata.lastEvidenceAt.length > 0 ? { lastEvidenceAt: metadata.lastEvidenceAt } : {},
1769
+ ...typeof metadata.runId === "string" && metadata.runId.length > 0 ? { runId: metadata.runId } : {},
1770
+ ...typeof metadata.branchName === "string" && metadata.branchName.length > 0 ? { branchName: metadata.branchName } : {},
1771
+ ...typeof metadata.prNumber === "number" ? { prNumber: metadata.prNumber } : {},
1772
+ ...typeof metadata.prUrl === "string" && metadata.prUrl.length > 0 ? { prUrl: metadata.prUrl } : {},
1773
+ ...typeof metadata.reviewState === "string" && metadata.reviewState.length > 0 ? { reviewState: metadata.reviewState } : {},
1774
+ ...typeof metadata.blockerReason === "string" && metadata.blockerReason.length > 0 ? { blockerReason: metadata.blockerReason } : {},
1775
+ ...typeof metadata.sourceCommit === "string" && metadata.sourceCommit.length > 0 ? { sourceCommit: metadata.sourceCommit } : {}
1776
+ };
1788
1777
  }
1789
- function nativeWriteTreeCommit(repoPath, baseRef, updates, message) {
1790
- const requestDir = resolve10(sharedGitNativeOutputDir, "requests", `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`);
1791
- mkdirSync4(requestDir, { recursive: true });
1792
- const messagePath = resolve10(requestDir, "message.txt");
1793
- const updatesPath = resolve10(requestDir, "updates.json");
1794
- try {
1795
- writeFileSync5(messagePath, message, "utf8");
1796
- writeFileSync5(updatesPath, buildTreeCommitUpdatesJson(updates), "utf8");
1797
- return requireGitNativeString("write-tree-commit", [repoPath, baseRef, messagePath, updatesPath]);
1798
- } finally {
1799
- rmSync2(requestDir, { recursive: true, force: true });
1778
+ function discardMismatchedTaskStateMetadata(input) {
1779
+ input.taskId;
1780
+ const canonicalMetadata = canonicalizeTaskStateMetadata(input.metadata);
1781
+ if (!canonicalMetadata || !input.lifecycleStatus) {
1782
+ return null;
1783
+ }
1784
+ const metadataStatus = canonicalMetadata.status ?? null;
1785
+ if (metadataStatus && metadataStatus !== input.lifecycleStatus) {
1786
+ return null;
1800
1787
  }
1788
+ return canonicalMetadata;
1801
1789
  }
1802
- function nativePushRefWithLease(repoPath, localOid, remoteRef, expectedOldOid, remote = "origin") {
1803
- return requireGitNativeString("push-ref-with-lease", [
1804
- repoPath,
1805
- localOid,
1806
- remoteRef,
1807
- expectedOldOid,
1808
- remote
1809
- ]);
1790
+ function readTaskStateMetadataEnvelope(raw) {
1791
+ if (!raw || typeof raw !== "object") {
1792
+ return { schemaVersion: SUPPORTED_TASK_STATE_SCHEMA_VERSION, supported: true, baseTrackerCommit: null, tasks: {} };
1793
+ }
1794
+ const envelope = raw;
1795
+ const schemaVersion = typeof envelope.schemaVersion === "number" ? envelope.schemaVersion : SUPPORTED_TASK_STATE_SCHEMA_VERSION;
1796
+ if (schemaVersion !== SUPPORTED_TASK_STATE_SCHEMA_VERSION) {
1797
+ return { schemaVersion, supported: false, baseTrackerCommit: null, tasks: {} };
1798
+ }
1799
+ const rawTasks = envelope.tasks && typeof envelope.tasks === "object" && !Array.isArray(envelope.tasks) ? envelope.tasks : {};
1800
+ const tasks = Object.fromEntries(Object.entries(rawTasks).map(([taskId, metadata]) => [taskId, canonicalizeTaskStateMetadata(metadata)]).filter((entry) => entry[1] != null));
1801
+ return {
1802
+ schemaVersion,
1803
+ supported: true,
1804
+ baseTrackerCommit: typeof envelope.baseTrackerCommit === "string" && envelope.baseTrackerCommit.length > 0 ? envelope.baseTrackerCommit : null,
1805
+ tasks
1806
+ };
1810
1807
  }
1808
+ // packages/runtime/src/control-plane/state-sync/read.ts
1809
+ import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
1810
+ import { resolve as resolve16 } from "path";
1811
1811
 
1812
1812
  // packages/runtime/src/control-plane/native/utils.ts
1813
- import { ptr as ptr2 } from "bun:ffi";
1814
1813
  import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
1815
1814
  import { resolve as resolve13 } from "path";
1816
1815
 
@@ -1932,12 +1931,6 @@ var sharedNativeRuntimeOutputDir = resolve12(tmpdir4(), "rig-native");
1932
1931
  var sharedNativeRuntimeOutputPath = resolve12(sharedNativeRuntimeOutputDir, `runtime-native-${process.platform}-${process.arch}.${suffix}`);
1933
1932
  var colocatedNativeRuntimeFileName = `runtime-native.${suffix}`;
1934
1933
  var nativeRuntimeLibrary = await loadNativeRuntimeLibrary();
1935
- function requireNativeRuntimeLibrary(feature) {
1936
- if (!nativeRuntimeLibrary) {
1937
- throw new Error(`Native Zig runtime is required for ${feature}`);
1938
- }
1939
- return nativeRuntimeLibrary;
1940
- }
1941
1934
  async function ensureNativeRuntimeLibraryPath(outputPath = sharedNativeRuntimeOutputPath, options = {}) {
1942
1935
  if (await buildNativeRuntimeLibrary(outputPath, options)) {
1943
1936
  return outputPath;
@@ -2103,7 +2096,6 @@ function tryDlopenNativeRuntimeLibrary(outputPath) {
2103
2096
  function resolveMonorepoRoot2(projectRoot) {
2104
2097
  return resolveMonorepoRoot(projectRoot);
2105
2098
  }
2106
- var nativeScopeMatcher = null;
2107
2099
  var scopeRegexCache = new Map;
2108
2100
  function runCapture(command, cwd, env) {
2109
2101
  const result = Bun.spawnSync(command, {
@@ -2260,41 +2252,6 @@ function monorepoSearchCandidates(inputPath) {
2260
2252
  }
2261
2253
  return [...candidates];
2262
2254
  }
2263
- function scopeMatches(path, scopes) {
2264
- const matcher = getNativeScopeMatcher();
2265
- const pathVariants = unique([path, normalizeRelativeScopePath(path)]);
2266
- for (const scope of scopes) {
2267
- const scopeVariants = unique([scope, normalizeRelativeScopePath(scope)]);
2268
- for (const candidatePath of pathVariants) {
2269
- for (const candidateScope of scopeVariants) {
2270
- if (candidatePath === candidateScope) {
2271
- return true;
2272
- }
2273
- if (matcher.match(candidateScope, candidatePath)) {
2274
- return true;
2275
- }
2276
- }
2277
- }
2278
- }
2279
- return false;
2280
- }
2281
- function getNativeScopeMatcher() {
2282
- if (nativeScopeMatcher) {
2283
- return nativeScopeMatcher;
2284
- }
2285
- nativeScopeMatcher = createNativeScopeMatcher();
2286
- return nativeScopeMatcher;
2287
- }
2288
- function createNativeScopeMatcher() {
2289
- const library = requireNativeRuntimeLibrary("scope matching");
2290
- return {
2291
- match: (pattern, path) => {
2292
- const patternBuffer = Buffer.from(`${pattern}\x00`);
2293
- const pathBuffer = Buffer.from(`${path}\x00`);
2294
- return library.symbols.rig_scope_match(Number(ptr2(patternBuffer)), Number(ptr2(pathBuffer))) !== 0;
2295
- }
2296
- };
2297
- }
2298
2255
 
2299
2256
  // packages/runtime/src/control-plane/state-sync/repo.ts
2300
2257
  import { existsSync as existsSync13 } from "fs";
@@ -6931,20 +6888,7 @@ function filterTaskChangedFiles(projectRoot, taskId, files, scoped) {
6931
6888
  if (!taskId) {
6932
6889
  return unique(uniqueFiles).sort();
6933
6890
  }
6934
- const filteredFiles = unique(uniqueFiles).filter((file) => !isGeneratedTaskChangePath(taskId, file));
6935
- if (!scoped) {
6936
- return filteredFiles.sort();
6937
- }
6938
- const paths = resolveHarnessPaths(projectRoot);
6939
- const monorepoRoot = resolveTaskMonorepoRoot(projectRoot);
6940
- const scopes = readTaskConfigForInvocation(projectRoot)[taskId]?.scope || [];
6941
- if (scopes.length === 0) {
6942
- return [];
6943
- }
6944
- return filteredFiles.filter((file) => {
6945
- const normalized = normalizePathToScope(projectRoot, monorepoRoot || paths.monorepoRoot, file);
6946
- return scopeMatches(file, scopes) || scopeMatches(normalized, scopes);
6947
- }).sort();
6891
+ return unique(uniqueFiles).filter((file) => !isGeneratedTaskChangePath(taskId, file)).sort();
6948
6892
  }
6949
6893
  function resolveTaskMonorepoRoot(projectRoot) {
6950
6894
  const runtimeWorkspace = loadRuntimeContextFromEnv()?.workspaceDir || process.env.RIG_TASK_WORKSPACE?.trim();
@@ -6995,6 +6939,17 @@ function resolveMonorepoBaseCommit(projectRoot, repo) {
6995
6939
  }
6996
6940
  function collectWorkingTreeFiles(projectRoot, repo, baseline) {
6997
6941
  const files = new Set;
6942
+ const nativeFiles = nativePendingFiles(repo);
6943
+ if (nativeFiles !== null) {
6944
+ for (const entry of nativeFiles) {
6945
+ const normalized = normalizeChangedFilePath(entry.path);
6946
+ if (!normalized || baseline.has(normalized)) {
6947
+ continue;
6948
+ }
6949
+ files.add(normalized);
6950
+ }
6951
+ return [...files].sort();
6952
+ }
6998
6953
  for (const args of [
6999
6954
  ["diff", "--name-only"],
7000
6955
  ["diff", "--cached", "--name-only"],