@h-rig/cli 0.0.6-alpha.9 → 0.0.6-alpha.91
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.
- package/README.md +18 -19
- package/dist/bin/build-rig-binaries.d.ts +2 -0
- package/dist/bin/build-rig-binaries.js +22 -10
- package/dist/bin/rig.d.ts +72 -0
- package/dist/bin/rig.js +15637 -7344
- package/dist/config/rig-default-config.yml +5 -0
- package/dist/src/app/drone-ui.d.ts +34 -0
- package/dist/src/app/drone-ui.js +278 -0
- package/dist/src/commands/_async-ui.d.ts +10 -0
- package/dist/src/commands/_async-ui.js +121 -0
- package/dist/src/commands/_cli-format.d.ts +56 -0
- package/dist/src/commands/_cli-format.js +319 -0
- package/dist/src/commands/_connection-state.d.ts +54 -0
- package/dist/src/commands/_connection-state.js +75 -11
- package/dist/src/commands/_doctor-checks.d.ts +46 -0
- package/dist/src/commands/_doctor-checks.js +100 -267
- package/dist/src/commands/_help-catalog.d.ts +51 -0
- package/dist/src/commands/_help-catalog.js +502 -0
- package/dist/src/commands/_inprocess-services.d.ts +33 -0
- package/dist/src/commands/_inprocess-services.js +102 -0
- package/dist/src/commands/_json-output.d.ts +11 -0
- package/dist/src/commands/_json-output.js +60 -0
- package/dist/src/commands/_lazy-reconcile.d.ts +34 -0
- package/dist/src/commands/_lazy-reconcile.js +102 -0
- package/dist/src/commands/_parsers.d.ts +15 -0
- package/dist/src/commands/_parsers.js +18 -11
- package/dist/src/commands/_paths.d.ts +11 -0
- package/dist/src/commands/_paths.js +1 -1
- package/dist/src/commands/_pi-frontend.d.ts +35 -0
- package/dist/src/commands/_pi-frontend.js +64 -0
- package/dist/src/commands/_pi-install.d.ts +42 -0
- package/dist/src/commands/_pi-install.js +19 -36
- package/dist/src/commands/_policy.d.ts +8 -0
- package/dist/src/commands/_policy.js +67 -19
- package/dist/src/commands/_probes.d.ts +1 -0
- package/dist/src/commands/_run-bridge.d.ts +114 -0
- package/dist/src/commands/_run-bridge.js +387 -0
- package/dist/src/commands/_run-diagnostics.d.ts +9 -0
- package/dist/src/commands/_run-diagnostics.js +51 -0
- package/dist/src/commands/_run-driver-helpers.d.ts +26 -0
- package/dist/src/commands/_run-driver-helpers.js +79 -230
- package/dist/src/commands/_run-projection.d.ts +50 -0
- package/dist/src/commands/_run-projection.js +349 -0
- package/dist/src/commands/_run-subcommands.d.ts +3 -0
- package/dist/src/commands/_run-subcommands.js +31 -0
- package/dist/src/commands/_spinner.d.ts +25 -0
- package/dist/src/commands/_spinner.js +65 -0
- package/dist/src/commands/agent.d.ts +3 -0
- package/dist/src/commands/agent.js +8575 -236
- package/dist/src/commands/dist.d.ts +28 -0
- package/dist/src/commands/dist.js +44 -29
- package/dist/src/commands/doctor.d.ts +3 -0
- package/dist/src/commands/doctor.js +219 -268
- package/dist/src/commands/github.d.ts +3 -0
- package/dist/src/commands/github.js +236 -188
- package/dist/src/commands/inbox.d.ts +28 -0
- package/dist/src/commands/inbox.js +530 -111
- package/dist/src/commands/init.d.ts +64 -0
- package/dist/src/commands/init.js +912 -705
- package/dist/src/commands/inspect.d.ts +20 -0
- package/dist/src/commands/inspect.js +709 -119
- package/dist/src/commands/pi.d.ts +3 -0
- package/dist/src/commands/pi.js +177 -0
- package/dist/src/commands/plugin.d.ts +16 -0
- package/dist/src/commands/plugin.js +576 -29
- package/dist/src/commands/profile-and-review.d.ts +4 -0
- package/dist/src/commands/profile-and-review.js +112 -67
- package/dist/src/commands/queue.d.ts +3 -0
- package/dist/src/commands/queue.js +20 -20
- package/dist/src/commands/remote.d.ts +3 -0
- package/dist/src/commands/remote.js +880 -50
- package/dist/src/commands/repo-git-harness.d.ts +5 -0
- package/dist/src/commands/repo-git-harness.js +78 -28
- package/dist/src/commands/run.d.ts +21 -0
- package/dist/src/commands/run.js +17701 -661
- package/dist/src/commands/server.d.ts +3 -0
- package/dist/src/commands/server.js +166 -303
- package/dist/src/commands/setup.d.ts +16 -0
- package/dist/src/commands/setup.js +244 -299
- package/dist/src/commands/stats.d.ts +15 -0
- package/dist/src/commands/stats.js +979 -0
- package/dist/src/commands/task-run-driver.d.ts +94 -0
- package/dist/src/commands/task-run-driver.js +116 -1961
- package/dist/src/commands/task.d.ts +35 -0
- package/dist/src/commands/task.js +676 -1201
- package/dist/src/commands/test.d.ts +3 -0
- package/dist/src/commands/test.js +15 -8
- package/dist/src/commands/triage.d.ts +11 -0
- package/dist/src/commands/triage.js +227 -0
- package/dist/src/commands/workspace.d.ts +3 -0
- package/dist/src/commands/workspace.js +18 -11
- package/dist/src/commands.d.ts +13 -0
- package/dist/src/commands.js +16792 -8055
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +16862 -8450
- package/dist/src/launcher.d.ts +61 -0
- package/dist/src/launcher.js +81 -13
- package/dist/src/operator-cli.d.ts +2 -0
- package/dist/src/operator-cli.js +17837 -0
- package/dist/src/operator-entry.d.ts +1 -0
- package/dist/src/operator-entry.js +3 -0
- package/dist/src/runner.d.ts +47 -0
- package/dist/src/runner.js +16 -22
- package/dist/src/withMutedConsole.d.ts +2 -0
- package/package.json +21 -8
- package/dist/src/commands/_authority-runs.js +0 -111
- package/dist/src/commands/_operator-view.js +0 -340
- package/dist/src/commands/_preflight.js +0 -478
- package/dist/src/commands/_server-client.js +0 -382
- package/dist/src/commands/_snapshot-upload.js +0 -331
- package/dist/src/commands/_task-picker.js +0 -48
- package/dist/src/commands/browser.js +0 -890
- package/dist/src/commands/connect.js +0 -180
- package/dist/src/commands/inspector.js +0 -256
- package/dist/src/commands/task-report-bug.js +0 -1083
- package/dist/src/report-bug.js +0 -260
|
@@ -1,36 +1,85 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/cli/src/commands/_policy.ts
|
|
3
|
-
import { appendFileSync, mkdirSync } from "fs";
|
|
4
|
-
import { resolve } from "path";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
4
|
+
import { resolve as resolve2 } from "path";
|
|
5
5
|
|
|
6
6
|
// packages/cli/src/runner.ts
|
|
7
7
|
import { EventBus } from "@rig/runtime/control-plane/runtime/events";
|
|
8
|
-
import { CliError } from "@rig/runtime/control-plane/errors";
|
|
8
|
+
import { CliError as RuntimeCliError } from "@rig/runtime/control-plane/errors";
|
|
9
9
|
import { evaluate, loadPolicy, resolveAction } from "@rig/runtime/control-plane/runtime/guard";
|
|
10
|
-
import { PluginManager } from "@rig/runtime/control-plane/runtime/plugins";
|
|
11
|
-
import { loadRuntimeContextFromEnv } from "@rig/runtime/control-plane/runtime/context";
|
|
12
10
|
import { buildBinary } from "@rig/runtime/control-plane/runtime/isolation";
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
class CliError extends RuntimeCliError {
|
|
13
|
+
hint;
|
|
14
|
+
constructor(message, exitCode = 1, options = {}) {
|
|
15
|
+
super(message, exitCode);
|
|
16
|
+
if (options.hint?.trim()) {
|
|
17
|
+
this.hint = options.hint.trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
function formatCommand(parts) {
|
|
15
22
|
return parts.map((part) => /[^a-zA-Z0-9_./:-]/.test(part) ? JSON.stringify(part) : part).join(" ");
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
// packages/cli/src/commands/_paths.ts
|
|
26
|
+
import { resolve } from "path";
|
|
27
|
+
import { resolveMonorepoRoot } from "@rig/runtime/layout";
|
|
28
|
+
function resolveControlPlaneHostStateRoot(projectRoot) {
|
|
29
|
+
return resolve(projectRoot, ".rig");
|
|
30
|
+
}
|
|
31
|
+
function resolveControlPlaneHostLogsDir(projectRoot) {
|
|
32
|
+
return resolve(resolveControlPlaneHostStateRoot(projectRoot), "logs");
|
|
33
|
+
}
|
|
34
|
+
function resolveControlPlaneDefinitionRoot(projectRoot) {
|
|
35
|
+
return resolve(projectRoot, "rig");
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
// packages/cli/src/commands/_policy.ts
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
function loadPolicyFile(projectRoot) {
|
|
40
|
+
const policyPath = resolve2(resolveControlPlaneDefinitionRoot(projectRoot), "policy", "policy.json");
|
|
41
|
+
if (!existsSync(policyPath)) {
|
|
42
|
+
return { mode: "observe", rules: [] };
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(readFileSync(policyPath, "utf8"));
|
|
46
|
+
const mode = parsed.mode === "off" || parsed.mode === "observe" || parsed.mode === "enforce" ? parsed.mode : "observe";
|
|
47
|
+
const rules = Array.isArray(parsed.rules) ? parsed.rules.filter((value) => Boolean(value && typeof value === "object" && !Array.isArray(value))).map((value, index) => ({
|
|
48
|
+
id: typeof value.id === "string" && value.id.trim() ? value.id.trim() : `rule-${index + 1}`,
|
|
49
|
+
reason: typeof value.reason === "string" && value.reason.trim() ? value.reason.trim() : undefined,
|
|
50
|
+
match: typeof value.match === "string" && value.match.trim() ? value.match.trim() : undefined,
|
|
51
|
+
command: typeof value.command === "string" && value.command.trim() ? value.command.trim() : undefined
|
|
52
|
+
})) : [];
|
|
53
|
+
return { mode, rules };
|
|
54
|
+
} catch {
|
|
55
|
+
return { mode: "observe", rules: [] };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function matchingRules(command, rules) {
|
|
59
|
+
return rules.filter((rule) => {
|
|
60
|
+
const rawPattern = rule.command ?? rule.match;
|
|
61
|
+
if (!rawPattern)
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
return new RegExp(rawPattern, "i").test(command);
|
|
65
|
+
} catch {
|
|
66
|
+
return command.includes(rawPattern);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
21
70
|
function resolveEffectivePolicyMode(context) {
|
|
22
71
|
const envMode = process.env.RIG_BASH_MODE;
|
|
23
72
|
if (envMode === "off" || envMode === "observe" || envMode === "enforce") {
|
|
24
73
|
return envMode;
|
|
25
74
|
}
|
|
26
|
-
return context.policyMode
|
|
75
|
+
return context.policyMode ?? loadPolicyFile(context.projectRoot).mode;
|
|
27
76
|
}
|
|
28
77
|
function appendControlledBashAudit(context, mode, command, args, matchedRuleIds, action) {
|
|
29
78
|
if (mode === "off") {
|
|
30
79
|
return;
|
|
31
80
|
}
|
|
32
|
-
const logsDir =
|
|
33
|
-
const logFile =
|
|
81
|
+
const logsDir = resolveControlPlaneHostLogsDir(context.projectRoot);
|
|
82
|
+
const logFile = resolve2(logsDir, "controlled-bash.jsonl");
|
|
34
83
|
mkdirSync(logsDir, { recursive: true });
|
|
35
84
|
const payload = {
|
|
36
85
|
timestamp: new Date().toISOString(),
|
|
@@ -51,25 +100,24 @@ function appendControlledBashAudit(context, mode, command, args, matchedRuleIds,
|
|
|
51
100
|
async function enforceNativeCommandPolicy(context, args, options) {
|
|
52
101
|
const mode = resolveEffectivePolicyMode(context);
|
|
53
102
|
const command = formatCommand([options.commandPrefix, ...args]);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
58
|
-
const action = resolveAction2(mode, decision.matchedRules);
|
|
59
|
-
const matchedRuleIds = decision.matchedRules.map((rule) => rule.id);
|
|
103
|
+
const rules = matchingRules(command, loadPolicyFile(context.projectRoot).rules);
|
|
104
|
+
const matchedRuleIds = rules.map((rule) => rule.id);
|
|
105
|
+
const action = rules.length === 0 ? "allow" : mode === "enforce" ? "block" : "warn";
|
|
60
106
|
await context.emitEvent("policy.decision", {
|
|
61
107
|
target: "command",
|
|
62
108
|
command,
|
|
63
109
|
mode,
|
|
64
110
|
allowed: action !== "block",
|
|
65
111
|
matchedRuleIds,
|
|
66
|
-
reasons:
|
|
112
|
+
reasons: rules.map((rule) => rule.reason).filter(Boolean)
|
|
67
113
|
});
|
|
68
114
|
if (options.writeAuditLog) {
|
|
69
115
|
appendControlledBashAudit(context, mode, command, args, matchedRuleIds, action === "block" ? "blocked" : action === "warn" ? "warn" : undefined);
|
|
70
116
|
}
|
|
71
117
|
if (action === "block") {
|
|
72
|
-
throw new
|
|
118
|
+
throw new CliError(`Policy blocked command: ${command}`, 126, {
|
|
119
|
+
hint: "Review rig/policy/policy.json, or re-run with `--policy-mode observe` to log instead of block."
|
|
120
|
+
});
|
|
73
121
|
}
|
|
74
122
|
}
|
|
75
123
|
export {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runQuietBinaryProbe(binaryPath: string, args: string[], cwd: string): Promise<boolean>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
|
2
|
+
import { type RegisteredTaskSource } from "@rig/contracts";
|
|
3
|
+
import { type CollabRegistryFilter, type LiveCollabProjection } from "@oh-my-pi/pi-coding-agent/collab/api";
|
|
4
|
+
export type RigExtensionTask = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly title?: string | null;
|
|
7
|
+
readonly status?: string | null;
|
|
8
|
+
readonly source?: string | null;
|
|
9
|
+
readonly url?: string | null;
|
|
10
|
+
readonly body?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type RigExtensionServerState = {
|
|
13
|
+
readonly alias: string;
|
|
14
|
+
readonly kind: "local" | "remote";
|
|
15
|
+
readonly baseUrl?: string | null;
|
|
16
|
+
readonly projectRoot?: string | null;
|
|
17
|
+
readonly status?: string | null;
|
|
18
|
+
readonly taskSource?: string | null;
|
|
19
|
+
};
|
|
20
|
+
export type RigExtensionAttachResult = {
|
|
21
|
+
readonly sessionPath?: string | null;
|
|
22
|
+
readonly joinLink?: string | null;
|
|
23
|
+
readonly collabSessionId?: string | null;
|
|
24
|
+
};
|
|
25
|
+
export type RunRunProcessInput = {
|
|
26
|
+
readonly projectRoot?: string;
|
|
27
|
+
readonly taskId: string;
|
|
28
|
+
readonly runId?: string;
|
|
29
|
+
readonly title?: string | null;
|
|
30
|
+
readonly model?: string | null;
|
|
31
|
+
readonly prompt?: string | null;
|
|
32
|
+
readonly rigRunBin?: string;
|
|
33
|
+
readonly writeStdout?: (text: string) => void;
|
|
34
|
+
};
|
|
35
|
+
export type RunRunProcessResult = {
|
|
36
|
+
readonly runId: string;
|
|
37
|
+
readonly pid: number;
|
|
38
|
+
readonly sessionName: string;
|
|
39
|
+
readonly runtime: unknown;
|
|
40
|
+
};
|
|
41
|
+
export type RunRunProcess = (input: RunRunProcessInput) => Promise<RunRunProcessResult>;
|
|
42
|
+
type SpawnFunction = (command: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
|
|
43
|
+
type ListTasksDeps = {
|
|
44
|
+
readonly loadTaskSource?: (projectRoot: string) => Promise<{
|
|
45
|
+
readonly kind: string;
|
|
46
|
+
readonly source: RegisteredTaskSource;
|
|
47
|
+
}>;
|
|
48
|
+
};
|
|
49
|
+
type SpawnRunProcessDeps = {
|
|
50
|
+
readonly runRunProcess?: RunRunProcess;
|
|
51
|
+
readonly spawn?: SpawnFunction;
|
|
52
|
+
readonly uuid?: () => string;
|
|
53
|
+
readonly resolveRigRunBin?: () => string;
|
|
54
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
55
|
+
};
|
|
56
|
+
type AttachViaRelayDeps = {
|
|
57
|
+
readonly listActiveCollabSessions?: (filter: CollabRegistryFilter) => Promise<readonly LiveCollabProjection[]>;
|
|
58
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
59
|
+
readonly now?: () => number;
|
|
60
|
+
};
|
|
61
|
+
export type SpawnRunProcessInput = {
|
|
62
|
+
readonly projectRoot: string;
|
|
63
|
+
readonly taskId: string;
|
|
64
|
+
readonly title?: string | null;
|
|
65
|
+
};
|
|
66
|
+
export type AttachViaRelayInput = {
|
|
67
|
+
readonly runId: string;
|
|
68
|
+
readonly taskId?: string | null;
|
|
69
|
+
readonly identityFilter: CollabRegistryFilter;
|
|
70
|
+
readonly timeoutMs?: number;
|
|
71
|
+
readonly pollIntervalMs?: number;
|
|
72
|
+
};
|
|
73
|
+
export declare function listTasksFromSource(projectRoot: string, deps?: ListTasksDeps): Promise<readonly RigExtensionTask[]>;
|
|
74
|
+
export declare function getTaskFromSource(projectRoot: string, taskId: string, deps?: ListTasksDeps): Promise<RigExtensionTask | null>;
|
|
75
|
+
export declare function readSelectedTarget(projectRoot: string, env?: NodeJS.ProcessEnv): Promise<RigExtensionServerState>;
|
|
76
|
+
export declare function resolveRigRunBin(): string;
|
|
77
|
+
export declare function spawnRunProcess(input: SpawnRunProcessInput, deps?: SpawnRunProcessDeps): Promise<{
|
|
78
|
+
readonly runId: string;
|
|
79
|
+
}>;
|
|
80
|
+
export type RunControl = {
|
|
81
|
+
readonly kind: "steer";
|
|
82
|
+
readonly message: string;
|
|
83
|
+
} | {
|
|
84
|
+
readonly kind: "stop";
|
|
85
|
+
readonly reason: string;
|
|
86
|
+
};
|
|
87
|
+
export interface DeliverRemoteControlDeps {
|
|
88
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
89
|
+
readonly spawn?: SpawnFunction;
|
|
90
|
+
}
|
|
91
|
+
interface RemoteControlContext {
|
|
92
|
+
readonly checkout?: string | null;
|
|
93
|
+
readonly registryBaseUrl?: string | null;
|
|
94
|
+
readonly namespaceKey?: string | null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Build the remote shell command that re-runs the operator's control VERB on the
|
|
98
|
+
* remote host. The run is LOCAL there (its session journal lives on the remote
|
|
99
|
+
* disk), so `rig run steer/stop <runId>` resolves it locally and the remote
|
|
100
|
+
* worker's own localRunChanges consumer applies it — fully symmetric with local
|
|
101
|
+
* placement. We SSH the verb (not a pre-built sentinel) so the remote rebuilds via
|
|
102
|
+
* its canonical stop path. Identity is passed inline (SSH does not forward local
|
|
103
|
+
* env); the registry HMAC secret is read from the remote env, never sent over argv.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildRemoteControlCommand(runId: string, control: RunControl, ctx: RemoteControlContext): string;
|
|
106
|
+
/**
|
|
107
|
+
* Deliver a steer/stop control to a REMOTE run by running the verb on the remote
|
|
108
|
+
* host over SSH (login shell, identical to spawnRunProcess). Awaits the remote
|
|
109
|
+
* exit code so the operator gets an honest success/failure — not fire-and-forget.
|
|
110
|
+
*/
|
|
111
|
+
export declare function deliverRemoteRunControl(projectRoot: string, runId: string, control: RunControl, deps?: DeliverRemoteControlDeps): Promise<void>;
|
|
112
|
+
export declare function listActiveRunCollab(filter: CollabRegistryFilter): Promise<readonly LiveCollabProjection[]>;
|
|
113
|
+
export declare function attachViaRelay(input: AttachViaRelayInput, deps?: AttachViaRelayDeps): Promise<RigExtensionAttachResult | null>;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/commands/_run-bridge.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
8
|
+
import { dirname, resolve } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { CUSTOM_TYPE_FOR, sessionIdFromSessionFile } from "@rig/contracts";
|
|
11
|
+
import { createPluginHost } from "@rig/core";
|
|
12
|
+
import { loadConfig } from "@rig/core/load-config";
|
|
13
|
+
import { resolveOwnerNamespaceKey, resolveRegistryBaseUrl, resolveRegistrySecret, resolveSelectedRemote } from "@rig/runtime/control-plane/remote";
|
|
14
|
+
import { listAgentRuntimes } from "@rig/runtime/control-plane/runtime/isolation";
|
|
15
|
+
import { createRegistryClient } from "@rig/relay-registry";
|
|
16
|
+
import {
|
|
17
|
+
listActiveCollabSessions as ompListActiveCollabSessions
|
|
18
|
+
} from "@oh-my-pi/pi-coding-agent/collab/api";
|
|
19
|
+
var DEFAULT_ATTACH_TIMEOUT_MS = 15000;
|
|
20
|
+
var DEFAULT_ATTACH_POLL_INTERVAL_MS = 250;
|
|
21
|
+
function stringField(value) {
|
|
22
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
23
|
+
}
|
|
24
|
+
function githubUserId(value) {
|
|
25
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
26
|
+
return String(value);
|
|
27
|
+
return stringField(value);
|
|
28
|
+
}
|
|
29
|
+
function readJsonRecord(path) {
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return null;
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
34
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function publicIdentityEnv(projectRoot) {
|
|
40
|
+
const auth = readJsonRecord(resolve(projectRoot, ".rig", "state", "github-auth.json"));
|
|
41
|
+
const connection = readJsonRecord(resolve(projectRoot, ".rig", "state", "connection.json"));
|
|
42
|
+
const values = {};
|
|
43
|
+
const selectedRepo = stringField(auth?.selectedRepo) ?? stringField(connection?.project);
|
|
44
|
+
const userId = githubUserId(auth?.userId);
|
|
45
|
+
const login = stringField(auth?.login);
|
|
46
|
+
const namespaceKey = stringField(auth?.userNamespaceKey) ?? resolveOwnerNamespaceKey(projectRoot);
|
|
47
|
+
if (selectedRepo)
|
|
48
|
+
values.RIG_SELECTED_REPO = selectedRepo;
|
|
49
|
+
if (userId)
|
|
50
|
+
values.RIG_GITHUB_USER_ID = userId;
|
|
51
|
+
if (login)
|
|
52
|
+
values.RIG_GITHUB_LOGIN = login;
|
|
53
|
+
if (namespaceKey)
|
|
54
|
+
values.RIG_GITHUB_NAMESPACE_KEY = namespaceKey;
|
|
55
|
+
return values;
|
|
56
|
+
}
|
|
57
|
+
function taskText(value) {
|
|
58
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
59
|
+
}
|
|
60
|
+
function taskUrl(record) {
|
|
61
|
+
const metadata = record;
|
|
62
|
+
return taskText(metadata.url) ?? taskText(metadata.html_url) ?? taskText(metadata.webUrl);
|
|
63
|
+
}
|
|
64
|
+
function taskBody(record) {
|
|
65
|
+
const metadata = record;
|
|
66
|
+
return taskText(metadata.body) ?? taskText(metadata.description);
|
|
67
|
+
}
|
|
68
|
+
function taskTitle(record) {
|
|
69
|
+
const metadata = record;
|
|
70
|
+
return taskText(metadata.title) ?? taskText(metadata.name) ?? record.id;
|
|
71
|
+
}
|
|
72
|
+
function toExtensionTask(record, sourceKind) {
|
|
73
|
+
return {
|
|
74
|
+
id: record.id,
|
|
75
|
+
title: taskTitle(record),
|
|
76
|
+
status: typeof record.status === "string" ? record.status : null,
|
|
77
|
+
source: sourceKind,
|
|
78
|
+
url: taskUrl(record),
|
|
79
|
+
body: taskBody(record)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function loadConfiguredTaskSource(projectRoot) {
|
|
83
|
+
const normalizedRoot = resolve(projectRoot);
|
|
84
|
+
const config = await loadConfig(normalizedRoot);
|
|
85
|
+
const pluginHost = createPluginHost(config.plugins);
|
|
86
|
+
const taskSourceFactory = pluginHost.resolveTaskSourceFactoryByKind(config.taskSource.kind);
|
|
87
|
+
if (!taskSourceFactory) {
|
|
88
|
+
const kinds = pluginHost.listExecutableTaskSources().map((entry) => entry.kind).join(", ") || "none";
|
|
89
|
+
throw new Error(`No task source factory registered for kind "${config.taskSource.kind}". Registered kinds: ${kinds}.`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: config.taskSource.kind,
|
|
93
|
+
source: taskSourceFactory.factory(config.taskSource, { projectRoot: normalizedRoot })
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function listTasksFromSource(projectRoot, deps = {}) {
|
|
97
|
+
const { kind, source } = await (deps.loadTaskSource ?? loadConfiguredTaskSource)(projectRoot);
|
|
98
|
+
return (await source.list()).map((task) => toExtensionTask(task, kind));
|
|
99
|
+
}
|
|
100
|
+
async function getTaskFromSource(projectRoot, taskId, deps = {}) {
|
|
101
|
+
const tasks = await listTasksFromSource(projectRoot, deps);
|
|
102
|
+
return tasks.find((task) => task.id === taskId) ?? null;
|
|
103
|
+
}
|
|
104
|
+
async function readSelectedTarget(projectRoot, env = process.env) {
|
|
105
|
+
const config = await loadConfig(resolve(projectRoot));
|
|
106
|
+
const selected = resolveSelectedRemote(projectRoot, env);
|
|
107
|
+
return {
|
|
108
|
+
alias: selected?.alias ?? "local",
|
|
109
|
+
kind: selected ? "remote" : "local",
|
|
110
|
+
projectRoot,
|
|
111
|
+
status: "ready",
|
|
112
|
+
taskSource: config.taskSource.kind
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function runningFromCompiledBinary() {
|
|
116
|
+
return import.meta.url.includes("$bunfs");
|
|
117
|
+
}
|
|
118
|
+
function resolveRigRunBin() {
|
|
119
|
+
if (runningFromCompiledBinary())
|
|
120
|
+
return resolve(dirname(process.execPath), "rig-run");
|
|
121
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../../../rig-host/bin/rig-run.ts");
|
|
122
|
+
}
|
|
123
|
+
async function defaultRunRunProcess(input) {
|
|
124
|
+
const { runRunProcess } = await import("@rig/rig-host");
|
|
125
|
+
return await runRunProcess(input);
|
|
126
|
+
}
|
|
127
|
+
function shellQuote(value) {
|
|
128
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
129
|
+
}
|
|
130
|
+
function readProjectSlug(projectRoot) {
|
|
131
|
+
const connection = readJsonRecord(resolve(projectRoot, ".rig", "state", "connection.json"));
|
|
132
|
+
return stringField(connection?.project) ?? null;
|
|
133
|
+
}
|
|
134
|
+
function resolveProjectOriginUrl(projectRoot) {
|
|
135
|
+
const slug = readProjectSlug(projectRoot);
|
|
136
|
+
return slug && /^[^/]+\/[^/]+$/.test(slug) ? `https://github.com/${slug}.git` : null;
|
|
137
|
+
}
|
|
138
|
+
async function spawnRunProcess(input, deps = {}) {
|
|
139
|
+
const runId = (deps.uuid ?? randomUUID)();
|
|
140
|
+
const env = deps.env ?? process.env;
|
|
141
|
+
const identityEnv = publicIdentityEnv(input.projectRoot);
|
|
142
|
+
const selected = resolveSelectedRemote(input.projectRoot, env);
|
|
143
|
+
const sshTarget = selected?.sshTarget ?? env.RIG_REMOTE_ALIAS?.trim();
|
|
144
|
+
if (sshTarget) {
|
|
145
|
+
const namespaceKey = identityEnv.RIG_GITHUB_NAMESPACE_KEY ?? resolveOwnerNamespaceKey(input.projectRoot, env) ?? "";
|
|
146
|
+
const sh = shellQuote;
|
|
147
|
+
const originUrl = resolveProjectOriginUrl(input.projectRoot);
|
|
148
|
+
const repoName = (readProjectSlug(input.projectRoot)?.split("/").pop() ?? "project").replace(/[^A-Za-z0-9._-]/g, "-");
|
|
149
|
+
const checkoutExpr = selected?.checkout ? sh(selected.checkout) : `"$HOME/.rig/checkouts/${repoName}"`;
|
|
150
|
+
const identityAssignments = Object.entries(identityEnv).map(([k, v]) => `${k}=${sh(v)}`);
|
|
151
|
+
if (namespaceKey && !identityEnv.RIG_GITHUB_NAMESPACE_KEY)
|
|
152
|
+
identityAssignments.push(`RIG_GITHUB_NAMESPACE_KEY=${sh(namespaceKey)}`);
|
|
153
|
+
const runInvocation = [
|
|
154
|
+
`RIG_RUN_ID=${sh(runId)}`,
|
|
155
|
+
`RIG_TASK_ID=${sh(input.taskId)}`,
|
|
156
|
+
...identityAssignments,
|
|
157
|
+
selected?.registryBaseUrl ? `RIG_REGISTRY_URL=${sh(selected.registryBaseUrl)}` : "",
|
|
158
|
+
`rig run start --task ${sh(input.taskId)}`
|
|
159
|
+
].filter(Boolean).join(" ");
|
|
160
|
+
const provisionAndRun = [
|
|
161
|
+
"set -e",
|
|
162
|
+
`CHECKOUT=${checkoutExpr}`,
|
|
163
|
+
originUrl ? `[ -d "$CHECKOUT/.git" ] || git clone ${sh(originUrl)} "$CHECKOUT"` : `[ -d "$CHECKOUT/.git" ] || { echo "rig: remote checkout $CHECKOUT missing and no origin to clone" >&2; exit 1; }`,
|
|
164
|
+
`cd "$CHECKOUT"`,
|
|
165
|
+
"git fetch origin --prune --quiet || true",
|
|
166
|
+
runInvocation
|
|
167
|
+
].join("; ");
|
|
168
|
+
const child = (deps.spawn ?? nodeSpawn)("ssh", [sshTarget, `bash -lc ${sh(provisionAndRun)}`], {
|
|
169
|
+
cwd: input.projectRoot,
|
|
170
|
+
env: { ...env, ...identityEnv },
|
|
171
|
+
detached: true,
|
|
172
|
+
stdio: "ignore"
|
|
173
|
+
});
|
|
174
|
+
child.unref();
|
|
175
|
+
return { runId };
|
|
176
|
+
}
|
|
177
|
+
await (deps.runRunProcess ?? defaultRunRunProcess)({
|
|
178
|
+
projectRoot: input.projectRoot,
|
|
179
|
+
taskId: input.taskId,
|
|
180
|
+
runId,
|
|
181
|
+
title: input.title,
|
|
182
|
+
rigRunBin: (deps.resolveRigRunBin ?? resolveRigRunBin)()
|
|
183
|
+
});
|
|
184
|
+
return { runId };
|
|
185
|
+
}
|
|
186
|
+
function buildRemoteControlCommand(runId, control, ctx) {
|
|
187
|
+
const verb = control.kind === "stop" ? `rig run stop ${shellQuote(runId)}${control.reason.trim() ? ` --reason ${shellQuote(control.reason)}` : ""}` : `rig run steer ${shellQuote(runId)} --message ${shellQuote(control.message)}`;
|
|
188
|
+
return [
|
|
189
|
+
ctx.checkout ? `cd ${shellQuote(ctx.checkout)} &&` : "",
|
|
190
|
+
ctx.namespaceKey ? `RIG_GITHUB_NAMESPACE_KEY=${shellQuote(ctx.namespaceKey)}` : "",
|
|
191
|
+
ctx.registryBaseUrl ? `RIG_REGISTRY_URL=${shellQuote(ctx.registryBaseUrl)}` : "",
|
|
192
|
+
verb
|
|
193
|
+
].filter(Boolean).join(" ");
|
|
194
|
+
}
|
|
195
|
+
async function deliverRemoteRunControl(projectRoot, runId, control, deps = {}) {
|
|
196
|
+
const env = deps.env ?? process.env;
|
|
197
|
+
const selected = resolveSelectedRemote(projectRoot, env);
|
|
198
|
+
const sshTarget = selected?.sshTarget ?? env.RIG_REMOTE_ALIAS?.trim();
|
|
199
|
+
if (!sshTarget) {
|
|
200
|
+
throw new Error(`Run ${runId} is remote but no SSH target is configured (set one with \`rig server use <alias>\`).`);
|
|
201
|
+
}
|
|
202
|
+
const identityEnv = publicIdentityEnv(projectRoot);
|
|
203
|
+
const namespaceKey = identityEnv.RIG_GITHUB_NAMESPACE_KEY ?? resolveOwnerNamespaceKey(projectRoot, env) ?? "";
|
|
204
|
+
const remoteCommand = buildRemoteControlCommand(runId, control, {
|
|
205
|
+
checkout: selected?.checkout ?? null,
|
|
206
|
+
registryBaseUrl: selected?.registryBaseUrl ?? null,
|
|
207
|
+
namespaceKey
|
|
208
|
+
});
|
|
209
|
+
await new Promise((resolveDone, rejectDone) => {
|
|
210
|
+
const child = (deps.spawn ?? nodeSpawn)("ssh", [sshTarget, `bash -lc ${shellQuote(remoteCommand)}`], {
|
|
211
|
+
cwd: projectRoot,
|
|
212
|
+
env: { ...env, ...identityEnv },
|
|
213
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
214
|
+
});
|
|
215
|
+
let stderr = "";
|
|
216
|
+
child.stderr?.on("data", (chunk) => {
|
|
217
|
+
stderr += String(chunk);
|
|
218
|
+
});
|
|
219
|
+
child.on("error", rejectDone);
|
|
220
|
+
child.on("close", (code) => {
|
|
221
|
+
if (code === 0) {
|
|
222
|
+
resolveDone();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const detail = stderr.trim();
|
|
226
|
+
rejectDone(new Error(`remote \`rig run ${control.kind}\` exited ${code ?? "null"}${detail ? `: ${detail}` : ""}`));
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
function registrySecret(env = process.env) {
|
|
231
|
+
return resolveRegistrySecret(process.cwd(), env) ?? null;
|
|
232
|
+
}
|
|
233
|
+
function registryBaseUrl(env = process.env) {
|
|
234
|
+
return resolveRegistryBaseUrl(process.cwd(), env);
|
|
235
|
+
}
|
|
236
|
+
function collabFromRegistryEntry(entry) {
|
|
237
|
+
return {
|
|
238
|
+
sessionId: entry.roomId,
|
|
239
|
+
sessionPath: "",
|
|
240
|
+
cwd: "",
|
|
241
|
+
title: entry.title,
|
|
242
|
+
joinLink: entry.joinLink,
|
|
243
|
+
webLink: entry.webLink,
|
|
244
|
+
relayUrl: entry.relayUrl,
|
|
245
|
+
owner: entry.owner,
|
|
246
|
+
selectedRepo: entry.repo,
|
|
247
|
+
startedAt: entry.startedAt,
|
|
248
|
+
updatedAt: entry.heartbeatAt,
|
|
249
|
+
...entry.pid === undefined ? {} : { pid: entry.pid },
|
|
250
|
+
stale: entry.stale
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function isRecord(value) {
|
|
254
|
+
return typeof value === "object" && value !== null;
|
|
255
|
+
}
|
|
256
|
+
function readLocalRunCollab(runtime) {
|
|
257
|
+
if (!runtime.sessionDir || !existsSync(runtime.sessionDir))
|
|
258
|
+
return null;
|
|
259
|
+
let files;
|
|
260
|
+
try {
|
|
261
|
+
files = readdirSync(runtime.sessionDir).filter((f) => f.endsWith(".jsonl")).sort().reverse();
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
for (const file of files) {
|
|
266
|
+
let lines;
|
|
267
|
+
try {
|
|
268
|
+
lines = readFileSync(resolve(runtime.sessionDir, file), "utf8").split(`
|
|
269
|
+
`);
|
|
270
|
+
} catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
let host = null;
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
if (!line.includes("collab-host-started"))
|
|
276
|
+
continue;
|
|
277
|
+
let entry;
|
|
278
|
+
try {
|
|
279
|
+
entry = JSON.parse(line);
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (!isRecord(entry) || entry.customType !== CUSTOM_TYPE_FOR["timeline-entry"] || !isRecord(entry.data))
|
|
284
|
+
continue;
|
|
285
|
+
const payload = isRecord(entry.data.payload) ? entry.data.payload : entry.data;
|
|
286
|
+
if (payload.type !== "collab-host-started")
|
|
287
|
+
continue;
|
|
288
|
+
host = {
|
|
289
|
+
roomId: typeof payload.roomId === "string" ? payload.roomId : undefined,
|
|
290
|
+
joinLink: typeof payload.joinLink === "string" ? payload.joinLink : undefined,
|
|
291
|
+
webLink: typeof payload.webLink === "string" ? payload.webLink : undefined,
|
|
292
|
+
relayUrl: typeof payload.relayUrl === "string" ? payload.relayUrl : undefined,
|
|
293
|
+
at: typeof entry.data.at === "string" ? entry.data.at : undefined
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (!host?.joinLink)
|
|
297
|
+
continue;
|
|
298
|
+
const at = host.at ?? "";
|
|
299
|
+
const sessionId = host.roomId ?? sessionIdFromSessionFile(resolve(runtime.sessionDir, file)) ?? runtime.id;
|
|
300
|
+
return {
|
|
301
|
+
sessionId,
|
|
302
|
+
sessionPath: resolve(runtime.sessionDir, file),
|
|
303
|
+
cwd: runtime.workspaceDir,
|
|
304
|
+
joinLink: host.joinLink,
|
|
305
|
+
webLink: host.webLink ?? "",
|
|
306
|
+
relayUrl: host.relayUrl ?? "",
|
|
307
|
+
title: runtime.taskId ? `[${runtime.taskId}] Rig run ${sessionId}` : `Rig run ${sessionId}`,
|
|
308
|
+
startedAt: at,
|
|
309
|
+
updatedAt: at,
|
|
310
|
+
stale: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
async function listLocalRunCollabSessions(projectRoot) {
|
|
316
|
+
let runtimes;
|
|
317
|
+
try {
|
|
318
|
+
runtimes = await listAgentRuntimes(projectRoot);
|
|
319
|
+
} catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const out = [];
|
|
323
|
+
for (const runtime of runtimes) {
|
|
324
|
+
const collab = readLocalRunCollab(runtime);
|
|
325
|
+
if (collab)
|
|
326
|
+
out.push(collab);
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
async function listActiveRunCollab(filter) {
|
|
331
|
+
return defaultListActiveCollabSessions(filter);
|
|
332
|
+
}
|
|
333
|
+
async function defaultListActiveCollabSessions(filter) {
|
|
334
|
+
const projectRoot = process.cwd();
|
|
335
|
+
const local = await listLocalRunCollabSessions(projectRoot);
|
|
336
|
+
const namespaceKey = filter.namespaceKey ?? resolveOwnerNamespaceKey(projectRoot);
|
|
337
|
+
const secret = registrySecret();
|
|
338
|
+
const remote = namespaceKey && secret ? (await createRegistryClient({ baseUrl: registryBaseUrl(), namespaceKey, secret }).listRoomsByOwner(filter)).map(collabFromRegistryEntry) : await ompListActiveCollabSessions(filter);
|
|
339
|
+
const seen = new Set(local.map((c) => c.sessionId));
|
|
340
|
+
return [...local, ...remote.filter((c) => !seen.has(c.sessionId))];
|
|
341
|
+
}
|
|
342
|
+
function sleep(ms) {
|
|
343
|
+
const { promise, resolve: resolveSleep } = Promise.withResolvers();
|
|
344
|
+
setTimeout(resolveSleep, ms);
|
|
345
|
+
return promise;
|
|
346
|
+
}
|
|
347
|
+
function matchesRun(collab, runId, taskId) {
|
|
348
|
+
if (collab.sessionId === runId)
|
|
349
|
+
return true;
|
|
350
|
+
if (runId && collab.title?.includes(runId))
|
|
351
|
+
return true;
|
|
352
|
+
return Boolean(taskId && collab.title?.includes(taskId));
|
|
353
|
+
}
|
|
354
|
+
async function attachViaRelay(input, deps = {}) {
|
|
355
|
+
const timeoutMs = input.timeoutMs ?? DEFAULT_ATTACH_TIMEOUT_MS;
|
|
356
|
+
const pollIntervalMs = input.pollIntervalMs ?? DEFAULT_ATTACH_POLL_INTERVAL_MS;
|
|
357
|
+
const listActive = deps.listActiveCollabSessions ?? defaultListActiveCollabSessions;
|
|
358
|
+
const now = deps.now ?? Date.now;
|
|
359
|
+
const pause = deps.sleep ?? sleep;
|
|
360
|
+
const deadline = now() + timeoutMs;
|
|
361
|
+
do {
|
|
362
|
+
const live = await listActive(input.identityFilter);
|
|
363
|
+
const match = live.find((collab) => matchesRun(collab, input.runId, input.taskId) && !collab.stale);
|
|
364
|
+
if (match) {
|
|
365
|
+
return {
|
|
366
|
+
sessionPath: match.sessionPath || null,
|
|
367
|
+
joinLink: match.joinLink || null,
|
|
368
|
+
collabSessionId: match.sessionId
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (now() >= deadline)
|
|
372
|
+
break;
|
|
373
|
+
await pause(pollIntervalMs);
|
|
374
|
+
} while (now() <= deadline);
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
export {
|
|
378
|
+
spawnRunProcess,
|
|
379
|
+
resolveRigRunBin,
|
|
380
|
+
readSelectedTarget,
|
|
381
|
+
listTasksFromSource,
|
|
382
|
+
listActiveRunCollab,
|
|
383
|
+
getTaskFromSource,
|
|
384
|
+
deliverRemoteRunControl,
|
|
385
|
+
buildRemoteControlCommand,
|
|
386
|
+
attachViaRelay
|
|
387
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunJournalProjection } from "@rig/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Extract the most useful human-facing failure diagnostic from the folded run journal.
|
|
4
|
+
*
|
|
5
|
+
* The live projection deliberately does not retain log/timeline lines, so this mirrors the
|
|
6
|
+
* deleted server-side categorization using only fields that survive folding: record fields,
|
|
7
|
+
* status reasons, closeout details, and reducer anomalies.
|
|
8
|
+
*/
|
|
9
|
+
export declare function summarizeUsefulRunError(projection: RunJournalProjection): string | null;
|