@twelvehart/orcats 0.0.0-seed.0 → 0.2.3
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/LICENSE +48 -0
- package/NOTICE +5 -0
- package/README.md +399 -0
- package/bin/orcats +4 -0
- package/dist/backends/acp-client.d.ts +74 -0
- package/dist/backends/acp-client.d.ts.map +1 -0
- package/dist/backends/acp-run.d.ts +31 -0
- package/dist/backends/acp-run.d.ts.map +1 -0
- package/dist/backends/claude-run.d.ts +35 -0
- package/dist/backends/claude-run.d.ts.map +1 -0
- package/dist/backends/claude-stream-json.d.ts +15 -0
- package/dist/backends/claude-stream-json.d.ts.map +1 -0
- package/dist/backends/codex-jsonl.d.ts +34 -0
- package/dist/backends/codex-jsonl.d.ts.map +1 -0
- package/dist/backends/codex-run.d.ts +40 -0
- package/dist/backends/codex-run.d.ts.map +1 -0
- package/dist/backends/codex.d.ts +5 -0
- package/dist/backends/codex.d.ts.map +1 -0
- package/dist/backends/conversation-config.d.ts +12 -0
- package/dist/backends/conversation-config.d.ts.map +1 -0
- package/dist/backends/index.d.ts +14 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/opencode-run.d.ts +48 -0
- package/dist/backends/opencode-run.d.ts.map +1 -0
- package/dist/backends/opencode-sse.d.ts +35 -0
- package/dist/backends/opencode-sse.d.ts.map +1 -0
- package/dist/backends/pi-rpc.d.ts +21 -0
- package/dist/backends/pi-rpc.d.ts.map +1 -0
- package/dist/backends/pi-run.d.ts +21 -0
- package/dist/backends/pi-run.d.ts.map +1 -0
- package/dist/backends/select.d.ts +17 -0
- package/dist/backends/select.d.ts.map +1 -0
- package/dist/backends/subprocess-run.d.ts +68 -0
- package/dist/backends/subprocess-run.d.ts.map +1 -0
- package/dist/backends/types.d.ts +17 -0
- package/dist/backends/types.d.ts.map +1 -0
- package/dist/backends/unsupported.d.ts +9 -0
- package/dist/backends/unsupported.d.ts.map +1 -0
- package/dist/baseline/index.d.ts +56 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/embedded.d.ts +7 -0
- package/dist/cli/embedded.d.ts.map +1 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/conversation/ask-user.d.ts +17 -0
- package/dist/conversation/ask-user.d.ts.map +1 -0
- package/dist/conversation/conversation.d.ts +46 -0
- package/dist/conversation/conversation.d.ts.map +1 -0
- package/dist/conversation/harness.d.ts +14 -0
- package/dist/conversation/harness.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +5 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/queue.d.ts +15 -0
- package/dist/conversation/queue.d.ts.map +1 -0
- package/dist/flow/accessors.d.ts +13 -0
- package/dist/flow/accessors.d.ts.map +1 -0
- package/dist/flow/approvals.d.ts +6 -0
- package/dist/flow/approvals.d.ts.map +1 -0
- package/dist/flow/args.d.ts +4 -0
- package/dist/flow/args.d.ts.map +1 -0
- package/dist/flow/context.d.ts +36 -0
- package/dist/flow/context.d.ts.map +1 -0
- package/dist/flow/index.d.ts +5 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/loop/builder/index.d.ts +17 -0
- package/dist/loop/builder/index.d.ts.map +1 -0
- package/dist/loop/builder/types.d.ts +97 -0
- package/dist/loop/builder/types.d.ts.map +1 -0
- package/dist/loop/context/compaction.d.ts +38 -0
- package/dist/loop/context/compaction.d.ts.map +1 -0
- package/dist/loop/context/index.d.ts +4 -0
- package/dist/loop/context/index.d.ts.map +1 -0
- package/dist/loop/context/offload.d.ts +34 -0
- package/dist/loop/context/offload.d.ts.map +1 -0
- package/dist/loop/context/types.d.ts +13 -0
- package/dist/loop/context/types.d.ts.map +1 -0
- package/dist/loop/engine/bridge.d.ts +21 -0
- package/dist/loop/engine/bridge.d.ts.map +1 -0
- package/dist/loop/engine/index.d.ts +73 -0
- package/dist/loop/engine/index.d.ts.map +1 -0
- package/dist/loop/execution.d.ts +68 -0
- package/dist/loop/execution.d.ts.map +1 -0
- package/dist/loop/fanout.d.ts +148 -0
- package/dist/loop/fanout.d.ts.map +1 -0
- package/dist/loop/firing.d.ts +54 -0
- package/dist/loop/firing.d.ts.map +1 -0
- package/dist/loop/graph/analyze.d.ts +8 -0
- package/dist/loop/graph/analyze.d.ts.map +1 -0
- package/dist/loop/graph/index.d.ts +3 -0
- package/dist/loop/graph/index.d.ts.map +1 -0
- package/dist/loop/graph/types.d.ts +35 -0
- package/dist/loop/graph/types.d.ts.map +1 -0
- package/dist/loop/index.d.ts +7 -0
- package/dist/loop/index.d.ts.map +1 -0
- package/dist/loop/io/index.d.ts +4 -0
- package/dist/loop/io/index.d.ts.map +1 -0
- package/dist/loop/io/linear.d.ts +142 -0
- package/dist/loop/io/linear.d.ts.map +1 -0
- package/dist/loop/io/sink.d.ts +47 -0
- package/dist/loop/io/sink.d.ts.map +1 -0
- package/dist/loop/io/source.d.ts +64 -0
- package/dist/loop/io/source.d.ts.map +1 -0
- package/dist/loop/serve.d.ts +97 -0
- package/dist/loop/serve.d.ts.map +1 -0
- package/dist/loop/state/index.d.ts +5 -0
- package/dist/loop/state/index.d.ts.map +1 -0
- package/dist/loop/state/manifest.d.ts +36 -0
- package/dist/loop/state/manifest.d.ts.map +1 -0
- package/dist/loop/state/port.d.ts +33 -0
- package/dist/loop/state/port.d.ts.map +1 -0
- package/dist/loop/state/snapshot.d.ts +15 -0
- package/dist/loop/state/snapshot.d.ts.map +1 -0
- package/dist/loop/state/sqlite.d.ts +31 -0
- package/dist/loop/state/sqlite.d.ts.map +1 -0
- package/dist/loop/termination-contract.d.ts +28 -0
- package/dist/loop/termination-contract.d.ts.map +1 -0
- package/dist/loop/termination.d.ts +33 -0
- package/dist/loop/termination.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +30 -0
- package/dist/model/backend-config.d.ts.map +1 -0
- package/dist/model/brand.d.ts +9 -0
- package/dist/model/brand.d.ts.map +1 -0
- package/dist/model/errors.d.ts +11 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/index.d.ts +8 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/json-schema.d.ts +4 -0
- package/dist/model/json-schema.d.ts.map +1 -0
- package/dist/model/result.d.ts +3 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/schemas.d.ts +435 -0
- package/dist/model/schemas.d.ts.map +1 -0
- package/dist/model/structured.d.ts +9 -0
- package/dist/model/structured.d.ts.map +1 -0
- package/dist/monitor/index.d.ts +139 -0
- package/dist/monitor/index.d.ts.map +1 -0
- package/dist/plan/index.d.ts +3 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/persistent.d.ts +23 -0
- package/dist/plan/persistent.d.ts.map +1 -0
- package/dist/plan/verdict.d.ts +9 -0
- package/dist/plan/verdict.d.ts.map +1 -0
- package/dist/review/index.d.ts +4 -0
- package/dist/review/index.d.ts.map +1 -0
- package/dist/review/loop.d.ts +117 -0
- package/dist/review/loop.d.ts.map +1 -0
- package/dist/review/reviewers.d.ts +13 -0
- package/dist/review/reviewers.d.ts.map +1 -0
- package/dist/review/tool.d.ts +20 -0
- package/dist/review/tool.d.ts.map +1 -0
- package/dist/run-output/index.d.ts +112 -0
- package/dist/run-output/index.d.ts.map +1 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/typecheck.d.ts +23 -0
- package/dist/runner/typecheck.d.ts.map +1 -0
- package/dist/test-utils/fakes.d.ts +76 -0
- package/dist/test-utils/fakes.d.ts.map +1 -0
- package/dist/test-utils/fixtures.d.ts +4 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +4 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/tier2.d.ts +8 -0
- package/dist/test-utils/tier2.d.ts.map +1 -0
- package/dist/tools/fs.d.ts +11 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/git.d.ts +10 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/github.d.ts +13 -0
- package/dist/tools/github.d.ts.map +1 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/linear.d.ts +117 -0
- package/dist/tools/linear.d.ts.map +1 -0
- package/dist/tools/process.d.ts +40 -0
- package/dist/tools/process.d.ts.map +1 -0
- package/dist/tools/terminal.d.ts +23 -0
- package/dist/tools/terminal.d.ts.map +1 -0
- package/package.json +89 -9
- package/src/backends/acp-client.ts +324 -0
- package/src/backends/acp-run.ts +636 -0
- package/src/backends/claude-run.ts +226 -0
- package/src/backends/claude-stream-json.ts +273 -0
- package/src/backends/codex-jsonl.ts +339 -0
- package/src/backends/codex-run.ts +177 -0
- package/src/backends/codex.ts +84 -0
- package/src/backends/conversation-config.ts +26 -0
- package/src/backends/index.ts +13 -0
- package/src/backends/opencode-run.ts +432 -0
- package/src/backends/opencode-sse.ts +360 -0
- package/src/backends/pi-rpc.ts +242 -0
- package/src/backends/pi-run.ts +154 -0
- package/src/backends/select.ts +77 -0
- package/src/backends/subprocess-run.ts +305 -0
- package/src/backends/types.ts +29 -0
- package/src/backends/unsupported.ts +48 -0
- package/src/baseline/index.ts +437 -0
- package/src/cli/args.ts +109 -0
- package/src/cli/embedded.ts +206 -0
- package/src/cli/main.ts +332 -0
- package/src/cli/version.ts +1 -0
- package/src/conversation/ask-user.ts +105 -0
- package/src/conversation/conversation.ts +109 -0
- package/src/conversation/harness.ts +39 -0
- package/src/conversation/index.ts +4 -0
- package/src/conversation/queue.ts +86 -0
- package/src/flow/accessors.ts +43 -0
- package/src/flow/approvals.ts +10 -0
- package/src/flow/args.ts +26 -0
- package/src/flow/context.ts +106 -0
- package/src/flow/index.ts +4 -0
- package/src/index.ts +17 -0
- package/src/loop/builder/index.ts +346 -0
- package/src/loop/builder/types.ts +131 -0
- package/src/loop/context/compaction.ts +175 -0
- package/src/loop/context/index.ts +3 -0
- package/src/loop/context/offload.ts +79 -0
- package/src/loop/context/types.ts +17 -0
- package/src/loop/engine/bridge.ts +61 -0
- package/src/loop/engine/index.ts +166 -0
- package/src/loop/execution.ts +453 -0
- package/src/loop/fanout.ts +444 -0
- package/src/loop/firing.ts +188 -0
- package/src/loop/graph/analyze.ts +128 -0
- package/src/loop/graph/index.ts +2 -0
- package/src/loop/graph/types.ts +45 -0
- package/src/loop/index.ts +11 -0
- package/src/loop/io/index.ts +3 -0
- package/src/loop/io/linear.ts +714 -0
- package/src/loop/io/sink.ts +170 -0
- package/src/loop/io/source.ts +255 -0
- package/src/loop/serve.ts +307 -0
- package/src/loop/state/index.ts +4 -0
- package/src/loop/state/manifest.ts +56 -0
- package/src/loop/state/port.ts +43 -0
- package/src/loop/state/snapshot.ts +129 -0
- package/src/loop/state/sqlite.ts +265 -0
- package/src/loop/termination-contract.ts +89 -0
- package/src/loop/termination.ts +156 -0
- package/src/model/backend-config.ts +34 -0
- package/src/model/brand.ts +11 -0
- package/src/model/errors.ts +28 -0
- package/src/model/index.ts +7 -0
- package/src/model/json-schema.ts +14 -0
- package/src/model/result.ts +28 -0
- package/src/model/schemas.ts +218 -0
- package/src/model/structured.ts +26 -0
- package/src/monitor/index.ts +367 -0
- package/src/plan/index.ts +2 -0
- package/src/plan/persistent.ts +98 -0
- package/src/plan/verdict.ts +3 -0
- package/src/review/index.ts +3 -0
- package/src/review/loop.ts +365 -0
- package/src/review/reviewers.ts +92 -0
- package/src/review/tool.ts +41 -0
- package/src/run-output/index.ts +319 -0
- package/src/runner/index.ts +1 -0
- package/src/runner/typecheck.ts +72 -0
- package/src/test-utils/fakes.ts +260 -0
- package/src/test-utils/fixtures.ts +33 -0
- package/src/test-utils/index.ts +3 -0
- package/src/test-utils/tier2.ts +12 -0
- package/src/tools/fs.ts +44 -0
- package/src/tools/git.ts +34 -0
- package/src/tools/github.ts +25 -0
- package/src/tools/index.ts +6 -0
- package/src/tools/linear.ts +428 -0
- package/src/tools/process.ts +156 -0
- package/src/tools/terminal.ts +116 -0
package/src/tools/fs.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { err, ok, type Result } from "neverthrow";
|
|
4
|
+
import type { RuntimeError } from "../model/index.ts";
|
|
5
|
+
|
|
6
|
+
export interface FsTool {
|
|
7
|
+
readText(path: string): Promise<Result<string, RuntimeError>>;
|
|
8
|
+
writeText(path: string, content: string, options?: { readonly mode?: number }): Promise<Result<void, RuntimeError>>;
|
|
9
|
+
exists(path: string): Promise<boolean>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createFsTool(): FsTool {
|
|
13
|
+
return {
|
|
14
|
+
async readText(path) {
|
|
15
|
+
try {
|
|
16
|
+
return ok(await readFile(path, "utf8"));
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return err({ _tag: "FileSystemError", path, message: String(error) });
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async writeText(path, content, options) {
|
|
22
|
+
try {
|
|
23
|
+
await mkdir(dirname(path), { recursive: true });
|
|
24
|
+
if (options?.mode === undefined) {
|
|
25
|
+
await writeFile(path, content);
|
|
26
|
+
} else {
|
|
27
|
+
await writeFile(path, content, { mode: options.mode });
|
|
28
|
+
await chmod(path, options.mode);
|
|
29
|
+
}
|
|
30
|
+
return ok(undefined);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return err({ _tag: "FileSystemError", path, message: String(error) });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
async exists(path) {
|
|
36
|
+
try {
|
|
37
|
+
await access(path);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/tools/git.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { err, type Result } from "neverthrow";
|
|
2
|
+
import type { RuntimeError } from "../model/index.ts";
|
|
3
|
+
import { runQuiet, type QuietProcResult } from "./process.ts";
|
|
4
|
+
|
|
5
|
+
export interface GitTool {
|
|
6
|
+
status(): Promise<Result<string, RuntimeError>>;
|
|
7
|
+
add(paths: readonly string[]): Promise<Result<void, RuntimeError>>;
|
|
8
|
+
commit(message: string): Promise<Result<QuietProcResult, RuntimeError>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createGitTool(cwd: string): GitTool {
|
|
12
|
+
return {
|
|
13
|
+
async status() {
|
|
14
|
+
const result = await runQuiet("git", ["status", "--short"], { cwd });
|
|
15
|
+
return result.map(({ stdout }) => stdout);
|
|
16
|
+
},
|
|
17
|
+
async add(paths) {
|
|
18
|
+
const result = await runQuiet("git", ["add", ...paths], { cwd });
|
|
19
|
+
return result.map(() => undefined);
|
|
20
|
+
},
|
|
21
|
+
async commit(message) {
|
|
22
|
+
const status = await runQuiet("git", ["status", "--short"], { cwd });
|
|
23
|
+
if (status.isErr()) {
|
|
24
|
+
return err(status.error);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (status.value.stdout.trim() === "") {
|
|
28
|
+
return err({ _tag: "NothingToCommit" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return await runQuiet("git", ["commit", "-m", message], { cwd });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Result } from "neverthrow";
|
|
2
|
+
import type { RuntimeError } from "../model/index.ts";
|
|
3
|
+
import { runQuiet, type QuietProcResult } from "./process.ts";
|
|
4
|
+
|
|
5
|
+
export interface PullRequestInput {
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly bodyFile: string;
|
|
8
|
+
readonly base?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GitHubTool {
|
|
12
|
+
createPullRequest(input: PullRequestInput): Promise<Result<QuietProcResult, RuntimeError>>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createGitHubTool(cwd: string): GitHubTool {
|
|
16
|
+
return {
|
|
17
|
+
createPullRequest(input) {
|
|
18
|
+
const args = ["pr", "create", "--title", input.title, "--body-file", input.bodyFile];
|
|
19
|
+
if (input.base) {
|
|
20
|
+
args.push("--base", input.base);
|
|
21
|
+
}
|
|
22
|
+
return runQuiet("gh", args, { cwd });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { err, ok, type Result } from "neverthrow";
|
|
2
|
+
|
|
3
|
+
import { ioFailed, type RuntimeError } from "../model/index.ts";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LINEAR_ENDPOINT = "https://api.linear.app/graphql";
|
|
6
|
+
|
|
7
|
+
export type LinearAuth =
|
|
8
|
+
| { readonly kind: "apiKey"; readonly token: string }
|
|
9
|
+
| { readonly kind: "oauth"; readonly token: string };
|
|
10
|
+
|
|
11
|
+
export interface LinearGraphQLRequest {
|
|
12
|
+
readonly endpoint: string;
|
|
13
|
+
readonly query: string;
|
|
14
|
+
readonly variables?: Record<string, unknown>;
|
|
15
|
+
readonly headers: Record<string, string>;
|
|
16
|
+
readonly operationName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LinearGraphQLError {
|
|
20
|
+
readonly message: string;
|
|
21
|
+
readonly path?: readonly (string | number)[];
|
|
22
|
+
readonly extensions?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LinearGraphQLResponse<TData = unknown> {
|
|
26
|
+
readonly data?: TData | null;
|
|
27
|
+
readonly errors?: readonly LinearGraphQLError[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type LinearGraphQLTransport = <TData = unknown>(
|
|
31
|
+
request: LinearGraphQLRequest,
|
|
32
|
+
) => Promise<Result<LinearGraphQLResponse<TData>, RuntimeError>>;
|
|
33
|
+
|
|
34
|
+
export interface LinearTeamRef {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly key?: string | null;
|
|
37
|
+
readonly name?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LinearProjectRef {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly name?: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LinearWorkflowState {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly type?: string | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LinearIssue {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly identifier?: string | null;
|
|
54
|
+
readonly title?: string | null;
|
|
55
|
+
readonly description?: string | null;
|
|
56
|
+
readonly url?: string | null;
|
|
57
|
+
readonly team?: LinearTeamRef | null;
|
|
58
|
+
readonly project?: LinearProjectRef | null;
|
|
59
|
+
readonly state?: LinearWorkflowState | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LinearIssueUpdateInput {
|
|
63
|
+
readonly issueId: string;
|
|
64
|
+
readonly title?: string;
|
|
65
|
+
readonly description?: string;
|
|
66
|
+
readonly stateId?: string;
|
|
67
|
+
readonly assigneeId?: string | null;
|
|
68
|
+
readonly projectId?: string | null;
|
|
69
|
+
readonly priority?: number | null;
|
|
70
|
+
readonly labelIds?: readonly string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LinearIssueComment {
|
|
74
|
+
readonly id: string;
|
|
75
|
+
readonly body?: string | null;
|
|
76
|
+
readonly url?: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface LinearAgentActivity {
|
|
80
|
+
readonly id: string;
|
|
81
|
+
readonly type?: string | null;
|
|
82
|
+
readonly body?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface LinearExternalUrl {
|
|
86
|
+
readonly label?: string;
|
|
87
|
+
readonly url: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LinearAgentSession {
|
|
91
|
+
readonly id: string;
|
|
92
|
+
readonly plan?: string | null;
|
|
93
|
+
readonly externalUrl?: string | null;
|
|
94
|
+
readonly externalUrls?: readonly LinearExternalUrl[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface LinearAgentActivityInput {
|
|
98
|
+
readonly agentSessionId: string;
|
|
99
|
+
readonly type?: "action" | "response" | "error";
|
|
100
|
+
readonly body: string;
|
|
101
|
+
readonly metadata?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface LinearAgentSessionUpdateInput {
|
|
105
|
+
readonly agentSessionId: string;
|
|
106
|
+
readonly plan?: string;
|
|
107
|
+
readonly externalUrl?: string;
|
|
108
|
+
readonly externalUrls?: readonly LinearExternalUrl[];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface LinearTool {
|
|
112
|
+
fetchIssue(input: { readonly issueId: string }): Promise<Result<LinearIssue | null, RuntimeError>>;
|
|
113
|
+
updateIssue(input: LinearIssueUpdateInput): Promise<Result<LinearIssue, RuntimeError>>;
|
|
114
|
+
createIssueComment(input: {
|
|
115
|
+
readonly issueId: string;
|
|
116
|
+
readonly body: string;
|
|
117
|
+
}): Promise<Result<LinearIssueComment, RuntimeError>>;
|
|
118
|
+
createAgentActivity(input: LinearAgentActivityInput): Promise<Result<LinearAgentActivity, RuntimeError>>;
|
|
119
|
+
updateAgentSession(input: LinearAgentSessionUpdateInput): Promise<Result<LinearAgentSession, RuntimeError>>;
|
|
120
|
+
getTeamWorkflowStates(input: {
|
|
121
|
+
readonly teamId: string;
|
|
122
|
+
}): Promise<Result<readonly LinearWorkflowState[], RuntimeError>>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface LinearToolOptions {
|
|
126
|
+
readonly auth?: LinearAuth;
|
|
127
|
+
readonly endpoint?: string;
|
|
128
|
+
readonly transport?: LinearGraphQLTransport;
|
|
129
|
+
readonly env?: Record<string, string | undefined>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function linearAuthFromEnv(env: Record<string, string | undefined> = process.env): LinearAuth | undefined {
|
|
133
|
+
const oauth = env.LINEAR_ACCESS_TOKEN;
|
|
134
|
+
if (oauth !== undefined && oauth.length > 0) {
|
|
135
|
+
return { kind: "oauth", token: oauth };
|
|
136
|
+
}
|
|
137
|
+
const apiKey = env.LINEAR_API_KEY;
|
|
138
|
+
if (apiKey !== undefined && apiKey.length > 0) {
|
|
139
|
+
return { kind: "apiKey", token: apiKey };
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createFetchLinearGraphQLTransport(fetchImpl: typeof fetch = fetch): LinearGraphQLTransport {
|
|
145
|
+
return async <TData>(request: LinearGraphQLRequest) => {
|
|
146
|
+
try {
|
|
147
|
+
const response = await fetchImpl(request.endpoint, {
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: request.headers,
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
query: request.query,
|
|
152
|
+
variables: request.variables ?? {},
|
|
153
|
+
operationName: request.operationName,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
const body = await response.text();
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
return err(linearFailed(`Linear GraphQL HTTP ${String(response.status)}: ${body}`));
|
|
159
|
+
}
|
|
160
|
+
return ok(JSON.parse(body) as LinearGraphQLResponse<TData>);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return err(linearFailed(`Linear GraphQL transport failed: ${String(error)}`));
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createLinearTool(options: LinearToolOptions = {}): LinearTool {
|
|
168
|
+
const auth = options.auth ?? linearAuthFromEnv(options.env);
|
|
169
|
+
const endpoint = options.endpoint ?? DEFAULT_LINEAR_ENDPOINT;
|
|
170
|
+
const transport = options.transport ?? createFetchLinearGraphQLTransport();
|
|
171
|
+
const secrets = auth === undefined ? [] : [auth.token, authorizationHeader(auth)];
|
|
172
|
+
|
|
173
|
+
async function execute<TData>(
|
|
174
|
+
operationName: string,
|
|
175
|
+
query: string,
|
|
176
|
+
variables: Record<string, unknown> = {},
|
|
177
|
+
): Promise<Result<TData, RuntimeError>> {
|
|
178
|
+
if (auth === undefined) {
|
|
179
|
+
return err(linearFailed("Linear authentication is not configured"));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const sent = await transport<TData>({
|
|
183
|
+
endpoint,
|
|
184
|
+
query,
|
|
185
|
+
variables,
|
|
186
|
+
operationName,
|
|
187
|
+
headers: {
|
|
188
|
+
authorization: authorizationHeader(auth),
|
|
189
|
+
"content-type": "application/json",
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
if (sent.isErr()) {
|
|
193
|
+
return err(redactRuntimeError(sent.error, secrets));
|
|
194
|
+
}
|
|
195
|
+
const response = sent.value;
|
|
196
|
+
if (response.errors !== undefined && response.errors.length > 0) {
|
|
197
|
+
const messages = response.errors.map((item) => item.message).join("; ");
|
|
198
|
+
return err(linearFailed(redactSecrets(`Linear GraphQL ${operationName} failed: ${messages}`, secrets)));
|
|
199
|
+
}
|
|
200
|
+
return ok((response.data ?? {}) as TData);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
async fetchIssue(input) {
|
|
205
|
+
const result = await execute<{ readonly issue: LinearIssue | null }>(
|
|
206
|
+
"Issue",
|
|
207
|
+
`
|
|
208
|
+
query Issue($id: String!) {
|
|
209
|
+
issue(id: $id) {
|
|
210
|
+
id
|
|
211
|
+
identifier
|
|
212
|
+
title
|
|
213
|
+
description
|
|
214
|
+
url
|
|
215
|
+
team { id key name }
|
|
216
|
+
project { id name }
|
|
217
|
+
state { id name type }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
`,
|
|
221
|
+
{ id: input.issueId },
|
|
222
|
+
);
|
|
223
|
+
return result.map((data) => data.issue ?? null);
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
async updateIssue(input) {
|
|
227
|
+
const result = await execute<{
|
|
228
|
+
readonly issueUpdate?: { readonly success?: boolean; readonly issue?: LinearIssue | null } | null;
|
|
229
|
+
}>(
|
|
230
|
+
"IssueUpdate",
|
|
231
|
+
`
|
|
232
|
+
mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
233
|
+
issueUpdate(id: $id, input: $input) {
|
|
234
|
+
success
|
|
235
|
+
issue {
|
|
236
|
+
id
|
|
237
|
+
identifier
|
|
238
|
+
title
|
|
239
|
+
description
|
|
240
|
+
url
|
|
241
|
+
team { id key name }
|
|
242
|
+
project { id name }
|
|
243
|
+
state { id name type }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
`,
|
|
248
|
+
{ id: input.issueId, input: issueUpdateVariables(input) },
|
|
249
|
+
);
|
|
250
|
+
if (result.isErr()) return err(result.error);
|
|
251
|
+
const issue = result.value.issueUpdate?.issue;
|
|
252
|
+
if (issue === undefined || issue === null) {
|
|
253
|
+
return err(linearFailed("Linear IssueUpdate did not return an issue"));
|
|
254
|
+
}
|
|
255
|
+
return ok(issue);
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
async createIssueComment(input) {
|
|
259
|
+
const result = await execute<{
|
|
260
|
+
readonly commentCreate?: { readonly success?: boolean; readonly comment?: LinearIssueComment | null } | null;
|
|
261
|
+
}>(
|
|
262
|
+
"CommentCreate",
|
|
263
|
+
`
|
|
264
|
+
mutation CommentCreate($input: CommentCreateInput!) {
|
|
265
|
+
commentCreate(input: $input) {
|
|
266
|
+
success
|
|
267
|
+
comment { id body url }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
`,
|
|
271
|
+
{ input: { issueId: input.issueId, body: input.body } },
|
|
272
|
+
);
|
|
273
|
+
if (result.isErr()) return err(result.error);
|
|
274
|
+
const comment = result.value.commentCreate?.comment;
|
|
275
|
+
if (comment === undefined || comment === null) {
|
|
276
|
+
return err(linearFailed("Linear CommentCreate did not return a comment"));
|
|
277
|
+
}
|
|
278
|
+
return ok(comment);
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async createAgentActivity(input) {
|
|
282
|
+
const result = await execute<{
|
|
283
|
+
readonly agentActivityCreate?: {
|
|
284
|
+
readonly success?: boolean;
|
|
285
|
+
readonly agentActivity?: LinearAgentActivity | null;
|
|
286
|
+
} | null;
|
|
287
|
+
}>(
|
|
288
|
+
"AgentActivityCreate",
|
|
289
|
+
`
|
|
290
|
+
mutation AgentActivityCreate($input: AgentActivityCreateInput!) {
|
|
291
|
+
agentActivityCreate(input: $input) {
|
|
292
|
+
success
|
|
293
|
+
agentActivity { id type body }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
`,
|
|
297
|
+
{
|
|
298
|
+
input: {
|
|
299
|
+
agentSessionId: input.agentSessionId,
|
|
300
|
+
body: input.body,
|
|
301
|
+
type: input.type ?? "response",
|
|
302
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
if (result.isErr()) return err(result.error);
|
|
307
|
+
const activity = result.value.agentActivityCreate?.agentActivity;
|
|
308
|
+
if (activity === undefined || activity === null) {
|
|
309
|
+
return err(linearFailed("Linear AgentActivityCreate did not return an activity"));
|
|
310
|
+
}
|
|
311
|
+
return ok(activity);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async updateAgentSession(input) {
|
|
315
|
+
const result = await execute<{
|
|
316
|
+
readonly agentSessionUpdate?: {
|
|
317
|
+
readonly success?: boolean;
|
|
318
|
+
readonly agentSession?: LinearAgentSession | null;
|
|
319
|
+
} | null;
|
|
320
|
+
}>(
|
|
321
|
+
"AgentSessionUpdate",
|
|
322
|
+
`
|
|
323
|
+
mutation AgentSessionUpdate($id: String!, $input: AgentSessionUpdateInput!) {
|
|
324
|
+
agentSessionUpdate(id: $id, input: $input) {
|
|
325
|
+
success
|
|
326
|
+
agentSession { id plan externalUrl externalUrls { label url } }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
`,
|
|
330
|
+
{ id: input.agentSessionId, input: agentSessionUpdateVariables(input) },
|
|
331
|
+
);
|
|
332
|
+
if (result.isErr()) return err(result.error);
|
|
333
|
+
const session = result.value.agentSessionUpdate?.agentSession;
|
|
334
|
+
if (session === undefined || session === null) {
|
|
335
|
+
return err(linearFailed("Linear AgentSessionUpdate did not return a session"));
|
|
336
|
+
}
|
|
337
|
+
return ok(session);
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
async getTeamWorkflowStates(input) {
|
|
341
|
+
const result = await execute<{
|
|
342
|
+
readonly team?: {
|
|
343
|
+
readonly states?: { readonly nodes?: readonly LinearWorkflowState[] | null } | null;
|
|
344
|
+
} | null;
|
|
345
|
+
}>(
|
|
346
|
+
"TeamWorkflowStates",
|
|
347
|
+
`
|
|
348
|
+
query TeamWorkflowStates($id: String!) {
|
|
349
|
+
team(id: $id) {
|
|
350
|
+
states {
|
|
351
|
+
nodes { id name type }
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
`,
|
|
356
|
+
{ id: input.teamId },
|
|
357
|
+
);
|
|
358
|
+
return result.map((data) => data.team?.states?.nodes ?? []);
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function issueUpdateVariables(input: LinearIssueUpdateInput): Record<string, unknown> {
|
|
364
|
+
return pruneUndefined({
|
|
365
|
+
title: input.title,
|
|
366
|
+
description: input.description,
|
|
367
|
+
stateId: input.stateId,
|
|
368
|
+
assigneeId: input.assigneeId,
|
|
369
|
+
projectId: input.projectId,
|
|
370
|
+
priority: input.priority,
|
|
371
|
+
labelIds: input.labelIds,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function agentSessionUpdateVariables(input: LinearAgentSessionUpdateInput): Record<string, unknown> {
|
|
376
|
+
return pruneUndefined({
|
|
377
|
+
plan: input.plan,
|
|
378
|
+
externalUrl: input.externalUrl,
|
|
379
|
+
externalUrls: input.externalUrls,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function pruneUndefined(input: Record<string, unknown>): Record<string, unknown> {
|
|
384
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function authorizationHeader(auth: LinearAuth): string {
|
|
388
|
+
return auth.kind === "oauth" ? `Bearer ${auth.token}` : auth.token;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function linearFailed(message: string): RuntimeError {
|
|
392
|
+
return ioFailed("tool", "linear", message);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function redactRuntimeError(error: RuntimeError, secrets: readonly string[]): RuntimeError {
|
|
396
|
+
switch (error._tag) {
|
|
397
|
+
case "IoFailed":
|
|
398
|
+
return { ...error, message: redactSecrets(error.message, secrets) };
|
|
399
|
+
case "BackendFailed":
|
|
400
|
+
return { ...error, message: redactSecrets(error.message, secrets) };
|
|
401
|
+
case "CommandFailed":
|
|
402
|
+
return {
|
|
403
|
+
...error,
|
|
404
|
+
command: redactSecrets(error.command, secrets),
|
|
405
|
+
stdout: redactSecrets(error.stdout, secrets),
|
|
406
|
+
stderr: redactSecrets(error.stderr, secrets),
|
|
407
|
+
};
|
|
408
|
+
case "FileSystemError":
|
|
409
|
+
return { ...error, message: redactSecrets(error.message, secrets), path: redactSecrets(error.path, secrets) };
|
|
410
|
+
case "StructuredOutputValidationFailed":
|
|
411
|
+
case "TypecheckFailed":
|
|
412
|
+
case "UnsupportedFeature":
|
|
413
|
+
case "NothingToCommit":
|
|
414
|
+
case "BranchAlreadyExists":
|
|
415
|
+
case "PushRejected":
|
|
416
|
+
return error;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function redactSecrets(text: string, secrets: readonly string[]): string {
|
|
421
|
+
let redacted = text;
|
|
422
|
+
for (const secret of secrets) {
|
|
423
|
+
if (secret.length > 0) {
|
|
424
|
+
redacted = redacted.split(secret).join("[redacted]");
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return redacted;
|
|
428
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { err, ok, type Result } from "neverthrow";
|
|
3
|
+
import { commandFailed, type RuntimeError } from "../model/index.ts";
|
|
4
|
+
|
|
5
|
+
export interface QuietProcResult {
|
|
6
|
+
readonly stdout: string;
|
|
7
|
+
readonly stderr: string;
|
|
8
|
+
readonly exitCode: number | null;
|
|
9
|
+
readonly durationMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QuietProcOptions {
|
|
13
|
+
readonly cwd?: string;
|
|
14
|
+
readonly signal?: AbortSignal;
|
|
15
|
+
readonly timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface VerificationCommand {
|
|
19
|
+
readonly command: string;
|
|
20
|
+
readonly args?: readonly string[];
|
|
21
|
+
readonly cwd?: string;
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type VerificationCommandResult =
|
|
26
|
+
| {
|
|
27
|
+
readonly type: "success";
|
|
28
|
+
readonly command: string;
|
|
29
|
+
readonly stdout: string;
|
|
30
|
+
readonly stderr: string;
|
|
31
|
+
readonly exitCode: 0;
|
|
32
|
+
readonly durationMs: number;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
readonly type: "failed";
|
|
36
|
+
readonly command: string;
|
|
37
|
+
readonly stdout: string;
|
|
38
|
+
readonly stderr: string;
|
|
39
|
+
readonly exitCode: number | null;
|
|
40
|
+
readonly durationMs: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface CommandTool {
|
|
44
|
+
run(command: VerificationCommand): Promise<VerificationCommandResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runQuiet(
|
|
48
|
+
command: string,
|
|
49
|
+
args: readonly string[] = [],
|
|
50
|
+
options: QuietProcOptions = {}
|
|
51
|
+
): Promise<Result<QuietProcResult, RuntimeError>> {
|
|
52
|
+
const startedAt = Date.now();
|
|
53
|
+
const rendered = [command, ...args].join(" ");
|
|
54
|
+
const child = spawn(command, [...args], {
|
|
55
|
+
cwd: options.cwd,
|
|
56
|
+
signal: options.signal,
|
|
57
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
let stdout = "";
|
|
61
|
+
let stderr = "";
|
|
62
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
63
|
+
const timeoutState = { timedOut: false };
|
|
64
|
+
|
|
65
|
+
child.stdout.setEncoding("utf8");
|
|
66
|
+
child.stderr.setEncoding("utf8");
|
|
67
|
+
child.stdout.on("data", (chunk: string) => {
|
|
68
|
+
stdout += chunk;
|
|
69
|
+
});
|
|
70
|
+
child.stderr.on("data", (chunk: string) => {
|
|
71
|
+
stderr += chunk;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const timeoutMs = options.timeoutMs;
|
|
75
|
+
if (timeoutMs !== undefined) {
|
|
76
|
+
timeout = setTimeout(() => {
|
|
77
|
+
timeoutState.timedOut = true;
|
|
78
|
+
child.kill("SIGTERM");
|
|
79
|
+
}, Math.max(timeoutMs, 0));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const exit = Promise.withResolvers<number | null>();
|
|
84
|
+
child.on("error", exit.reject);
|
|
85
|
+
child.on("close", exit.resolve);
|
|
86
|
+
const exitCode = await exit.promise;
|
|
87
|
+
const durationMs = Date.now() - startedAt;
|
|
88
|
+
const result = { stdout, stderr, exitCode, durationMs };
|
|
89
|
+
if (exitCode === 0 && !timeoutState.timedOut) {
|
|
90
|
+
return ok(result);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const failureStderr = timeoutState.timedOut
|
|
94
|
+
? [stderr.trimEnd(), `Command timed out after ${String(timeoutMs)}ms`]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join("\n")
|
|
97
|
+
: stderr;
|
|
98
|
+
return err(
|
|
99
|
+
commandFailed({
|
|
100
|
+
command: rendered,
|
|
101
|
+
exitCode,
|
|
102
|
+
stdout,
|
|
103
|
+
stderr: failureStderr
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createCommandTool(defaultCwd = process.cwd()): CommandTool {
|
|
112
|
+
return {
|
|
113
|
+
async run(command) {
|
|
114
|
+
const args = command.args ?? [];
|
|
115
|
+
const rendered = [command.command, ...args].join(" ");
|
|
116
|
+
const startedAt = Date.now();
|
|
117
|
+
const result = await runQuiet(command.command, args, {
|
|
118
|
+
cwd: command.cwd ?? defaultCwd,
|
|
119
|
+
...(command.timeoutMs === undefined ? {} : { timeoutMs: command.timeoutMs })
|
|
120
|
+
});
|
|
121
|
+
const fallbackDurationMs = Date.now() - startedAt;
|
|
122
|
+
|
|
123
|
+
if (result.isOk()) {
|
|
124
|
+
return {
|
|
125
|
+
type: "success",
|
|
126
|
+
command: rendered,
|
|
127
|
+
stdout: result.value.stdout,
|
|
128
|
+
stderr: result.value.stderr,
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
durationMs: result.value.durationMs
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const error = result.error;
|
|
135
|
+
if (error._tag === "CommandFailed") {
|
|
136
|
+
return {
|
|
137
|
+
type: "failed",
|
|
138
|
+
command: error.command,
|
|
139
|
+
stdout: error.stdout,
|
|
140
|
+
stderr: error.stderr,
|
|
141
|
+
exitCode: error.exitCode,
|
|
142
|
+
durationMs: fallbackDurationMs
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: "failed",
|
|
148
|
+
command: rendered,
|
|
149
|
+
stdout: "",
|
|
150
|
+
stderr: JSON.stringify(error),
|
|
151
|
+
exitCode: null,
|
|
152
|
+
durationMs: fallbackDurationMs
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|