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