opencode-teammate 0.1.0
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/.bunli/commands.gen.ts +87 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +140 -0
- package/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +4 -0
- package/.zed/settings.json +76 -0
- package/README.md +15 -0
- package/bunli.config.ts +11 -0
- package/bunup.config.ts +31 -0
- package/package.json +36 -0
- package/src/adapters/assets/index.ts +1 -0
- package/src/adapters/assets/specifications.ts +70 -0
- package/src/adapters/beads/agents.ts +105 -0
- package/src/adapters/beads/config.ts +17 -0
- package/src/adapters/beads/index.ts +4 -0
- package/src/adapters/beads/issues.ts +156 -0
- package/src/adapters/beads/specifications.ts +55 -0
- package/src/adapters/environments/index.ts +43 -0
- package/src/adapters/environments/worktrees.ts +78 -0
- package/src/adapters/teammates/index.ts +15 -0
- package/src/assets/agent/planner.md +196 -0
- package/src/assets/command/brainstorm.md +60 -0
- package/src/assets/command/specify.md +135 -0
- package/src/assets/command/work.md +247 -0
- package/src/assets/index.ts +37 -0
- package/src/cli/commands/manifest.ts +6 -0
- package/src/cli/commands/spec/sync.ts +47 -0
- package/src/cli/commands/work.ts +110 -0
- package/src/cli/index.ts +11 -0
- package/src/plugin.ts +45 -0
- package/src/tools/i-am-done.ts +44 -0
- package/src/tools/i-am-stuck.ts +49 -0
- package/src/tools/index.ts +2 -0
- package/src/use-cases/index.ts +5 -0
- package/src/use-cases/inject-beads-issue.ts +97 -0
- package/src/use-cases/sync-specifications.ts +48 -0
- package/src/use-cases/sync-teammates.ts +35 -0
- package/src/use-cases/track-specs.ts +91 -0
- package/src/use-cases/work-on-issue.ts +110 -0
- package/src/utils/chain.ts +60 -0
- package/src/utils/frontmatter.spec.ts +491 -0
- package/src/utils/frontmatter.ts +317 -0
- package/src/utils/opencode.ts +102 -0
- package/src/utils/polling.ts +41 -0
- package/src/utils/projects.ts +35 -0
- package/src/utils/shell/client.spec.ts +106 -0
- package/src/utils/shell/client.ts +117 -0
- package/src/utils/shell/error.ts +29 -0
- package/src/utils/shell/index.ts +2 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
3
|
+
import { assert, dedent } from "radashi";
|
|
4
|
+
|
|
5
|
+
import * as beads from "../adapters/beads";
|
|
6
|
+
|
|
7
|
+
export const createIAmStuck = (client: OpencodeClient) =>
|
|
8
|
+
tool({
|
|
9
|
+
description: dedent`
|
|
10
|
+
Inform your manager that you are stuck and cannot proceed with the task. The manager will then jump in to help you out.
|
|
11
|
+
Use this tool when:
|
|
12
|
+
- You need human intervention to proceed with the task.
|
|
13
|
+
- You face an issue that you cannot resolve on your own.
|
|
14
|
+
`,
|
|
15
|
+
args: {
|
|
16
|
+
reason: tool.schema
|
|
17
|
+
.string()
|
|
18
|
+
.describe("Describe why you are stuck and what kind of help you need."),
|
|
19
|
+
},
|
|
20
|
+
execute: async (args, ctx) => {
|
|
21
|
+
const session = await client.session.get({
|
|
22
|
+
sessionID: ctx.sessionID,
|
|
23
|
+
directory: ctx.directory,
|
|
24
|
+
});
|
|
25
|
+
assert(session.data, `Failed to fetch session: ${JSON.stringify(session.error)}`);
|
|
26
|
+
|
|
27
|
+
const message = await client.session.message({
|
|
28
|
+
sessionID: ctx.sessionID,
|
|
29
|
+
messageID: ctx.messageID,
|
|
30
|
+
directory: ctx.directory,
|
|
31
|
+
});
|
|
32
|
+
assert(message.data, `Failed to fetch session message: ${JSON.stringify(session.error)}`);
|
|
33
|
+
|
|
34
|
+
const { info } = message.data;
|
|
35
|
+
|
|
36
|
+
await beads.agents.stuck(Bun.$.cwd(session.data.directory), session.data.slug, {
|
|
37
|
+
reason: args.reason,
|
|
38
|
+
context: {
|
|
39
|
+
agent: ctx.agent,
|
|
40
|
+
model: info.role === "user" ? info.model.modelID : info.modelID,
|
|
41
|
+
provider: info.role === "user" ? info.model.providerID : info.providerID,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return dedent`
|
|
46
|
+
Your manager has been notified. Please wait for him to jump in and help you out.
|
|
47
|
+
`;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { assert, dedent, first } from "radashi";
|
|
2
|
+
import type { OpencodeClient, TextPartInput } from "@opencode-ai/sdk/v2/client";
|
|
3
|
+
import type { Part } from "@opencode-ai/sdk";
|
|
4
|
+
|
|
5
|
+
import * as beads from "../adapters/beads/index.js";
|
|
6
|
+
import * as chain from "../utils/chain.js";
|
|
7
|
+
|
|
8
|
+
const WHITE_LISTED_COMMANDS = new Set(["work", "fake_work"]);
|
|
9
|
+
|
|
10
|
+
export async function injectBeadsIssue(options: {
|
|
11
|
+
client: OpencodeClient;
|
|
12
|
+
directory: string;
|
|
13
|
+
input: {
|
|
14
|
+
sessionID: string;
|
|
15
|
+
command: string;
|
|
16
|
+
arguments: string;
|
|
17
|
+
};
|
|
18
|
+
output: {
|
|
19
|
+
parts: Part[];
|
|
20
|
+
};
|
|
21
|
+
}) {
|
|
22
|
+
const { input } = options;
|
|
23
|
+
|
|
24
|
+
assert(
|
|
25
|
+
!WHITE_LISTED_COMMANDS.has(input.command),
|
|
26
|
+
chain.createBegnign(`Command '${input.command}' is not withe-listed`),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const $ = Bun.$.cwd(options.directory);
|
|
30
|
+
const log = createLog(input.command);
|
|
31
|
+
|
|
32
|
+
const issueId = first(input.arguments.split(" "));
|
|
33
|
+
assert(issueId, chain.createBegnign(`Issue ID not found`));
|
|
34
|
+
|
|
35
|
+
const prefix = await beads.config.issuePrefix($);
|
|
36
|
+
assert(
|
|
37
|
+
issueId.startsWith(prefix.value),
|
|
38
|
+
chain.createBegnign(`Issue ID: ${issueId}, expected to start with '${prefix.value}' prefix`),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const alreadyInjected = options.output.parts.some(
|
|
42
|
+
(part) => part.type === "text" && part.metadata?.["injected_beads_issue_id"] === issueId,
|
|
43
|
+
);
|
|
44
|
+
assert(
|
|
45
|
+
alreadyInjected,
|
|
46
|
+
chain.createBegnign(`Injection for '${issueId}' already exists, skipped.`),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const [error, injection] = await beads.issues.show($, { id: issueId });
|
|
50
|
+
assert(error === undefined, chain.createError(`Failed to inject issue ${issueId}: ${error}`));
|
|
51
|
+
assert(injection, chain.createError(`Nothing to inject for issue '${issueId}'`));
|
|
52
|
+
|
|
53
|
+
const partToInject = createInjectionPart(issueId, injection);
|
|
54
|
+
|
|
55
|
+
// If the user provided only the issue ID, we replace it into the existing message
|
|
56
|
+
const shouldReplaceOnExistingMessage = options.input.arguments.trim() === issueId;
|
|
57
|
+
|
|
58
|
+
if (shouldReplaceOnExistingMessage) {
|
|
59
|
+
const partToEdit = options.output.parts.find(
|
|
60
|
+
(part) => part.type === "text" && part.text.includes(issueId),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (partToEdit && partToEdit.type === "text") {
|
|
64
|
+
partToEdit.text = partToEdit.text.replace(issueId, partToInject.text);
|
|
65
|
+
partToEdit.metadata = Object.assign({}, partToEdit.metadata, partToInject.metadata);
|
|
66
|
+
|
|
67
|
+
log(`Message updated with issue '${issueId}' injection`);
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// fallback on adding a new part on the message
|
|
74
|
+
options.output.parts.push(partToInject as unknown as Part);
|
|
75
|
+
|
|
76
|
+
log(`Message part added with issue '${issueId}' injection`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createInjectionPart(issueId: string, content: string): TextPartInput {
|
|
80
|
+
return {
|
|
81
|
+
type: "text",
|
|
82
|
+
text: dedent`
|
|
83
|
+
<issue>
|
|
84
|
+
${content}
|
|
85
|
+
</issue>
|
|
86
|
+
`,
|
|
87
|
+
metadata: {
|
|
88
|
+
injected_beads_issue_id: issueId,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createLog(command: string) {
|
|
94
|
+
return (message: string) => {
|
|
95
|
+
console.log(`[inject:beads_issue:${command}] ${message}`);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { dedent, title } from "radashi";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
import * as assets from "../adapters/assets";
|
|
5
|
+
import * as beads from "../adapters/beads";
|
|
6
|
+
import * as fm from "../utils/frontmatter";
|
|
7
|
+
|
|
8
|
+
export async function* syncSpecifications(options: {
|
|
9
|
+
project: string;
|
|
10
|
+
source: AsyncGenerator<assets.specifications.Specification>;
|
|
11
|
+
}) {
|
|
12
|
+
const shell = Bun.$.cwd(options.project);
|
|
13
|
+
|
|
14
|
+
for await (const asset of options.source) {
|
|
15
|
+
const { file, filepath } = asset;
|
|
16
|
+
const { metadata, content } = fm.extract(await file.text());
|
|
17
|
+
|
|
18
|
+
const relative = path.relative(options.project, path.format(filepath));
|
|
19
|
+
const spec: beads.specifications.SpecificationInput = {
|
|
20
|
+
file: relative,
|
|
21
|
+
title: title(filepath.name),
|
|
22
|
+
description: dedent`
|
|
23
|
+
Spec: ${relative}
|
|
24
|
+
`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const result = assets.specifications.Frontmatter.safeParse(metadata);
|
|
28
|
+
const existing =
|
|
29
|
+
result.success && result.data.issue
|
|
30
|
+
? { id: result.data.issue }
|
|
31
|
+
: await beads.specifications.find.byExternalRef(shell, spec.file);
|
|
32
|
+
|
|
33
|
+
const issue = existing
|
|
34
|
+
? await beads.specifications.update(shell, existing, spec)
|
|
35
|
+
: await beads.specifications.create(shell, spec);
|
|
36
|
+
|
|
37
|
+
const frontmatter = Object.assign({}, metadata, {
|
|
38
|
+
issue: issue.id,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await Bun.write(file, fm.apply(content, frontmatter));
|
|
42
|
+
|
|
43
|
+
yield {
|
|
44
|
+
asset,
|
|
45
|
+
issue,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// import type { Event } from "@opencode-ai/sdk";
|
|
2
|
+
// import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
|
+
// import type Bun from "bun";
|
|
4
|
+
|
|
5
|
+
// import * as beads from "../adapters/beads";
|
|
6
|
+
// import * as teammates from "../adapters/teammates";
|
|
7
|
+
|
|
8
|
+
// export async function syncTeammates({ $ }: PluginInput, event: Event) {
|
|
9
|
+
// let teammate: string | undefined;
|
|
10
|
+
// let status: beads.agents.AgentState | undefined;
|
|
11
|
+
|
|
12
|
+
// if (event.type === "session.created") {
|
|
13
|
+
// teammate = teammates.createFromSession(event.properties.info);
|
|
14
|
+
// status = "spawning";
|
|
15
|
+
// } else if (event.type === "session.status") {
|
|
16
|
+
// teammate = teammates.get(event.properties.sessionID);
|
|
17
|
+
|
|
18
|
+
// const { type } = event.properties.status;
|
|
19
|
+
|
|
20
|
+
// if (type === "busy") status = "running";
|
|
21
|
+
// else if (type === "idle") status = "idle";
|
|
22
|
+
// } else if (event.type === "session.idle") {
|
|
23
|
+
// teammate = teammates.get(event.properties.sessionID);
|
|
24
|
+
// status = "idle";
|
|
25
|
+
// } else if (event.type === "session.deleted") {
|
|
26
|
+
// teammate = teammates.get(event.properties.info.id);
|
|
27
|
+
// status = "stopped";
|
|
28
|
+
// }
|
|
29
|
+
|
|
30
|
+
// if (!teammate) return false;
|
|
31
|
+
// if (!status) return false;
|
|
32
|
+
|
|
33
|
+
// await beads.agents.updateState($ as Bun.$, teammate, status);
|
|
34
|
+
// return true;
|
|
35
|
+
// }
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { join, relative } from "path";
|
|
2
|
+
import Bun from "bun";
|
|
3
|
+
import type { Event } from "@opencode-ai/sdk";
|
|
4
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
5
|
+
|
|
6
|
+
import * as beads from "../adapters/beads";
|
|
7
|
+
import { crush, dedent, isObject, isString, set, shake } from "radashi";
|
|
8
|
+
|
|
9
|
+
export async function trackSpecs({ $, directory }: PluginInput, event: Event) {
|
|
10
|
+
if (event.type !== "file.edited") return undefined;
|
|
11
|
+
|
|
12
|
+
const { file } = event.properties;
|
|
13
|
+
|
|
14
|
+
const isSpec = file.startsWith(join(directory, "specs/"));
|
|
15
|
+
if (!isSpec) return undefined;
|
|
16
|
+
|
|
17
|
+
const path = relative(directory, file);
|
|
18
|
+
const { metadata, content } = extractFrontmatter(await Bun.file(file).text());
|
|
19
|
+
|
|
20
|
+
const spec: beads.specifications.SpecificationInput = {
|
|
21
|
+
file: path,
|
|
22
|
+
title: getSpecTitle(content) ?? `Untitled: ${path}`,
|
|
23
|
+
description: dedent`
|
|
24
|
+
${getSpecOverview(content)}
|
|
25
|
+
|
|
26
|
+
Spec: ${path}
|
|
27
|
+
`,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const existing =
|
|
31
|
+
metadata && "issue" in metadata && isString(metadata.issue) && metadata.issue
|
|
32
|
+
? { id: metadata.issue }
|
|
33
|
+
: await beads.specifications.find.byExternalRef($ as Bun.$, path);
|
|
34
|
+
|
|
35
|
+
const issue = existing
|
|
36
|
+
? await beads.specifications.update($ as Bun.$, existing, spec)
|
|
37
|
+
: await beads.specifications.create($ as Bun.$, spec);
|
|
38
|
+
|
|
39
|
+
const issueId = isObject(issue) && "id" in issue ? issue.id : undefined;
|
|
40
|
+
|
|
41
|
+
const frontmatter = createFrontmatter({
|
|
42
|
+
...metadata,
|
|
43
|
+
created: issue.created_at,
|
|
44
|
+
issue: issue.id,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await Bun.write(path, `${frontmatter}\n${content}`);
|
|
48
|
+
|
|
49
|
+
return issueId;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getSpecTitle(content: string) {
|
|
53
|
+
return content.match(/^# Feature Specification: (.+)$/m)?.[1];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSpecOverview(content: string) {
|
|
57
|
+
return content.match(/## Overview\n\n([\s\S]*?)(?=\n## |\n### |$)/)?.[1]?.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createFrontmatter(metadata: object) {
|
|
61
|
+
const entries = Object.entries(crush(shake(metadata)));
|
|
62
|
+
if (entries.length === 0) return "";
|
|
63
|
+
|
|
64
|
+
const lines = entries.map(([key, value]) => `${key}: ${value}`);
|
|
65
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractFrontmatter(content: string): {
|
|
69
|
+
metadata: object;
|
|
70
|
+
content: string;
|
|
71
|
+
} {
|
|
72
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
73
|
+
if (!frontmatterMatch) {
|
|
74
|
+
return { metadata: {}, content };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const frontmatterText = frontmatterMatch[1] ?? "";
|
|
78
|
+
const lines = frontmatterText.split("\n");
|
|
79
|
+
let result: Record<string, string> = {};
|
|
80
|
+
|
|
81
|
+
for (const line of lines) {
|
|
82
|
+
const [, key, value] = line.match(/^(\w+):\s*(.+)$/) ?? [];
|
|
83
|
+
if (key && value) {
|
|
84
|
+
result = set(result, key, value?.trim());
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const contentWithoutFrontmatter = content.slice(frontmatterMatch[0].length);
|
|
89
|
+
|
|
90
|
+
return { metadata: result, content: contentWithoutFrontmatter };
|
|
91
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { assert, dedent } from "radashi";
|
|
2
|
+
import type { OpencodeClient, TextPartInput } from "@opencode-ai/sdk/v2/client";
|
|
3
|
+
|
|
4
|
+
import * as beads from "../adapters/beads";
|
|
5
|
+
import * as environments from "../adapters/environments";
|
|
6
|
+
// import * as polling from "#utils/polling.js";
|
|
7
|
+
import * as polling from "../utils/polling.js";
|
|
8
|
+
|
|
9
|
+
export async function workOnIssue(options: {
|
|
10
|
+
client: OpencodeClient;
|
|
11
|
+
directory: string;
|
|
12
|
+
issue: { id: string } | beads.issues.Issue;
|
|
13
|
+
}) {
|
|
14
|
+
const log = createLog(options.issue.id);
|
|
15
|
+
|
|
16
|
+
log(`Start working on ${options.issue.id}...`);
|
|
17
|
+
|
|
18
|
+
await using env = await environments.WorkerEnvironment.init({
|
|
19
|
+
client: options.client,
|
|
20
|
+
directory: options.directory,
|
|
21
|
+
name: options.issue.id,
|
|
22
|
+
});
|
|
23
|
+
log(`Environment ready: ${env.worktree.directory}`);
|
|
24
|
+
|
|
25
|
+
let issue = await beads.issues.claim(env.$, options.issue);
|
|
26
|
+
log(`Issue claimed: ${issue.id}`);
|
|
27
|
+
|
|
28
|
+
const session = await options.client.session.create({
|
|
29
|
+
directory: env.worktree.directory,
|
|
30
|
+
title: `${issue.id}: ${issue.title}`,
|
|
31
|
+
});
|
|
32
|
+
assert(session.data, `Failed to start the session: ${JSON.stringify(session.error)}`);
|
|
33
|
+
log(`Session: ${session.data.id}`);
|
|
34
|
+
|
|
35
|
+
const command = await options.client.session.command({
|
|
36
|
+
sessionID: session.data.id,
|
|
37
|
+
directory: env.worktree.directory,
|
|
38
|
+
// agent: "<task_assignee>", // FIXME: assign
|
|
39
|
+
command: "fake_work", // FIXME: work
|
|
40
|
+
arguments: `${issue.id}`,
|
|
41
|
+
});
|
|
42
|
+
assert(command.data, `Failed to send the /work command: ${JSON.stringify(command.error)}`);
|
|
43
|
+
|
|
44
|
+
let agent = await beads.agents.use(env.$, session.data);
|
|
45
|
+
|
|
46
|
+
while (agent.agent_state !== "done") {
|
|
47
|
+
if (agent.agent_state === "stuck") {
|
|
48
|
+
log(`Agent is stuck: ${agent.title}`);
|
|
49
|
+
|
|
50
|
+
await polling.waitFor(
|
|
51
|
+
async () => {
|
|
52
|
+
agent = await beads.agents.use(env.$, session.data);
|
|
53
|
+
return agent.agent_state !== "stuck";
|
|
54
|
+
},
|
|
55
|
+
{ interval: "1s" },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
log(`Agent is ready to continue: ${agent.agent_state}`);
|
|
59
|
+
} else {
|
|
60
|
+
log(`Agent needs follow up, '${agent.agent_state}'...`);
|
|
61
|
+
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
const followUp = await options.client.session.prompt({
|
|
64
|
+
sessionID: session.data.id,
|
|
65
|
+
directory: env.worktree.directory,
|
|
66
|
+
parts: [createFollowUp(agent.agent_state)],
|
|
67
|
+
});
|
|
68
|
+
assert(followUp.data, `Failed to follow up: ${JSON.stringify(followUp.error)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
agent = await beads.agents.use(env.$, session.data);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log(`Agent work is done`);
|
|
75
|
+
|
|
76
|
+
const messages = await options.client.session.messages({
|
|
77
|
+
sessionID: session.data.id,
|
|
78
|
+
directory: env.worktree.directory,
|
|
79
|
+
});
|
|
80
|
+
assert(messages.data, `Failed to fetch session messages: ${messages.error}`);
|
|
81
|
+
|
|
82
|
+
log(`Session messages: ${messages.data.length}`);
|
|
83
|
+
|
|
84
|
+
issue = await beads.issues.close(env.$, options.issue, {
|
|
85
|
+
reason: "Completed",
|
|
86
|
+
});
|
|
87
|
+
assert(issue, `Failed to close the issue`);
|
|
88
|
+
|
|
89
|
+
// TODO: close the envrionment
|
|
90
|
+
|
|
91
|
+
return issue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createFollowUp(currentState: string) {
|
|
95
|
+
return {
|
|
96
|
+
type: "text",
|
|
97
|
+
text: dedent`
|
|
98
|
+
You are currently registered as '${currentState}'.
|
|
99
|
+
- If you are done with the task please notify via the i_am_done tool.
|
|
100
|
+
- If you are stuck and need human help, please notify via the i_am_stuck tool.
|
|
101
|
+
- If you need more time to work on the task, please continue working and update your status when needed.
|
|
102
|
+
`,
|
|
103
|
+
} satisfies TextPartInput;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function createLog(issueId: string) {
|
|
107
|
+
return (message: string) => {
|
|
108
|
+
console.log(`[work:${issueId}] ${message}`);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
type Chainable = () => Promise<void>;
|
|
2
|
+
|
|
3
|
+
export type ChainOptions = {
|
|
4
|
+
/** @default true */
|
|
5
|
+
stopOnFatal?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function chain(options: ChainOptions, ...fns: Chainable[]) {
|
|
9
|
+
const { stopOnFatal = true } = options;
|
|
10
|
+
|
|
11
|
+
for (const fn of fns) {
|
|
12
|
+
await fn().catch((error) => {
|
|
13
|
+
if (error instanceof ChainError) {
|
|
14
|
+
if (error.severity === "BEGNIGN") return;
|
|
15
|
+
if (error.severity === "WARNING") console.warn(error);
|
|
16
|
+
if (error.severity === "ERROR") console.error(error);
|
|
17
|
+
if (error.severity === "FATAL") {
|
|
18
|
+
console.error(error);
|
|
19
|
+
|
|
20
|
+
if (stopOnFatal) throw error;
|
|
21
|
+
}
|
|
22
|
+
} else throw error;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createBegnign(message: string, options?: ChainErrorOptions) {
|
|
28
|
+
return new ChainError(message, { severity: "BEGNIGN", ...options });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createError(message: string, options?: ChainErrorOptions) {
|
|
32
|
+
return new ChainError(message, { severity: "ERROR", ...options });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ChainErrorSeverity = "BEGNIGN" | "WARNING" | "ERROR" | "FATAL";
|
|
36
|
+
|
|
37
|
+
export interface ChainErrorOptions {
|
|
38
|
+
severity?: ChainErrorSeverity;
|
|
39
|
+
stop?: boolean;
|
|
40
|
+
cause?: Error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ChainError extends Error {
|
|
44
|
+
public readonly severity: ChainErrorSeverity;
|
|
45
|
+
public readonly stop: boolean;
|
|
46
|
+
|
|
47
|
+
constructor(
|
|
48
|
+
message: string,
|
|
49
|
+
options?: {
|
|
50
|
+
severity?: ChainErrorSeverity;
|
|
51
|
+
stop?: boolean;
|
|
52
|
+
cause?: Error;
|
|
53
|
+
},
|
|
54
|
+
) {
|
|
55
|
+
super(message, { cause: options?.cause });
|
|
56
|
+
|
|
57
|
+
this.severity = options?.severity ?? "BEGNIGN";
|
|
58
|
+
this.stop = options?.stop ?? false;
|
|
59
|
+
}
|
|
60
|
+
}
|