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.
Files changed (50) hide show
  1. package/.bunli/commands.gen.ts +87 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +140 -0
  4. package/.oxfmtrc.json +3 -0
  5. package/.oxlintrc.json +4 -0
  6. package/.zed/settings.json +76 -0
  7. package/README.md +15 -0
  8. package/bunli.config.ts +11 -0
  9. package/bunup.config.ts +31 -0
  10. package/package.json +36 -0
  11. package/src/adapters/assets/index.ts +1 -0
  12. package/src/adapters/assets/specifications.ts +70 -0
  13. package/src/adapters/beads/agents.ts +105 -0
  14. package/src/adapters/beads/config.ts +17 -0
  15. package/src/adapters/beads/index.ts +4 -0
  16. package/src/adapters/beads/issues.ts +156 -0
  17. package/src/adapters/beads/specifications.ts +55 -0
  18. package/src/adapters/environments/index.ts +43 -0
  19. package/src/adapters/environments/worktrees.ts +78 -0
  20. package/src/adapters/teammates/index.ts +15 -0
  21. package/src/assets/agent/planner.md +196 -0
  22. package/src/assets/command/brainstorm.md +60 -0
  23. package/src/assets/command/specify.md +135 -0
  24. package/src/assets/command/work.md +247 -0
  25. package/src/assets/index.ts +37 -0
  26. package/src/cli/commands/manifest.ts +6 -0
  27. package/src/cli/commands/spec/sync.ts +47 -0
  28. package/src/cli/commands/work.ts +110 -0
  29. package/src/cli/index.ts +11 -0
  30. package/src/plugin.ts +45 -0
  31. package/src/tools/i-am-done.ts +44 -0
  32. package/src/tools/i-am-stuck.ts +49 -0
  33. package/src/tools/index.ts +2 -0
  34. package/src/use-cases/index.ts +5 -0
  35. package/src/use-cases/inject-beads-issue.ts +97 -0
  36. package/src/use-cases/sync-specifications.ts +48 -0
  37. package/src/use-cases/sync-teammates.ts +35 -0
  38. package/src/use-cases/track-specs.ts +91 -0
  39. package/src/use-cases/work-on-issue.ts +110 -0
  40. package/src/utils/chain.ts +60 -0
  41. package/src/utils/frontmatter.spec.ts +491 -0
  42. package/src/utils/frontmatter.ts +317 -0
  43. package/src/utils/opencode.ts +102 -0
  44. package/src/utils/polling.ts +41 -0
  45. package/src/utils/projects.ts +35 -0
  46. package/src/utils/shell/client.spec.ts +106 -0
  47. package/src/utils/shell/client.ts +117 -0
  48. package/src/utils/shell/error.ts +29 -0
  49. package/src/utils/shell/index.ts +2 -0
  50. package/tsconfig.json +9 -0
@@ -0,0 +1,156 @@
1
+ import { assert, first, isEmpty } from "radashi";
2
+
3
+ // import * as shell from "#utils/shell/index.js";
4
+ import * as shell from "../../utils/shell/index.js";
5
+
6
+ export type Issue = {
7
+ id: string;
8
+ title: string;
9
+ status: string;
10
+ created_at: string;
11
+ external_ref?: string;
12
+ assignee?: string;
13
+ };
14
+
15
+ export interface ListOptions {
16
+ all?: boolean;
17
+ ids?: string[];
18
+ type?: string;
19
+ assignee?: string;
20
+ status?: "open" | "in_progress" | "blocked" | "deferred" | "closed";
21
+ sort?:
22
+ | "priority"
23
+ | "created"
24
+ | "updated"
25
+ | "closed"
26
+ | "status"
27
+ | "id"
28
+ | "title"
29
+ | "type"
30
+ | "assignee";
31
+ }
32
+
33
+ export interface ListReadyOptions {
34
+ parent?: string;
35
+ type?: string;
36
+ unassigned?: boolean;
37
+ assignee?: string;
38
+ sort?: "hybrid" | "priority" | "oldest";
39
+ }
40
+
41
+ export const list = Object.assign(
42
+ async (client: shell.client.ClientInput, options: ListOptions = {}) => {
43
+ if (isEmpty(options.ids)) return [];
44
+
45
+ const $ = shell.client.use(client);
46
+
47
+ const issues = await $<Issue[]>`bd list ${options} --json`.json();
48
+
49
+ return issues;
50
+ },
51
+ {
52
+ ready: async (client: shell.client.ClientInput, options: ListReadyOptions = {}) => {
53
+ const $ = shell.client.use(client);
54
+
55
+ const issues = await $<Issue[]>`bd ready ${options} --json`.json();
56
+
57
+ return issues;
58
+ },
59
+ },
60
+ );
61
+
62
+ export interface UseOptions {
63
+ /** Force re-fetching the issue */
64
+ force?: boolean;
65
+ }
66
+
67
+ export async function use(
68
+ client: shell.client.ClientInput,
69
+ input: { id: string } | Issue,
70
+ options?: UseOptions,
71
+ ) {
72
+ if ("status" in input && !options?.force) return input;
73
+
74
+ return first(
75
+ await list(client, {
76
+ ...options,
77
+ ids: [input.id],
78
+ }),
79
+ );
80
+ }
81
+
82
+ export interface UpdateOptions {
83
+ "add-label"?: string[] | string;
84
+ description?: string;
85
+ }
86
+
87
+ export async function update(
88
+ client: shell.client.ClientInput,
89
+ input: { id: string },
90
+ options: UpdateOptions = {},
91
+ ) {
92
+ const $ = shell.client.use(client);
93
+
94
+ const issues = await $<Issue[]>`bd update ${input.id} ${options} --json`.json();
95
+
96
+ const [issue] = issues;
97
+ assert(issue, `Issue '${input.id}' not found`);
98
+
99
+ return issue;
100
+ }
101
+
102
+ export interface ClaimOptions {
103
+ /** Assignee of the issue */
104
+ actor?: string;
105
+ }
106
+
107
+ export async function claim(
108
+ client: shell.client.ClientInput,
109
+ input: { id: string },
110
+ options: ClaimOptions = {},
111
+ ) {
112
+ const $ = shell.client.use(client);
113
+
114
+ const issues = await $<Issue[]>`bd update ${input.id} ${options} --claim --json`.json();
115
+
116
+ const [issue] = issues;
117
+ assert(issue, `Issue '${input.id}' not found`);
118
+
119
+ return issue;
120
+ }
121
+
122
+ export interface CloseOptions {
123
+ /** Reason for closing the issue */
124
+ reason?: string;
125
+ }
126
+
127
+ export async function close(
128
+ client: shell.client.ClientInput,
129
+ input: { id: string },
130
+ options: CloseOptions = {},
131
+ ) {
132
+ const $ = shell.client.use(client);
133
+
134
+ const issues = await $<Issue[]>`bd close ${input.id} ${options} --json`.json();
135
+
136
+ const [issue] = issues;
137
+ assert(issue, `Issue '${input.id}' not found`);
138
+
139
+ return issue;
140
+ }
141
+
142
+ export async function show(client: shell.client.ClientInput, input: { id: string }) {
143
+ const $ = shell.client.use(client);
144
+ return $`bd show ${input.id}`.text();
145
+ }
146
+
147
+ export namespace find {
148
+ export async function byExternalRef(
149
+ client: shell.client.ClientInput,
150
+ ref: string,
151
+ options?: ListOptions,
152
+ ) {
153
+ const _issues = await list(client, options);
154
+ return _issues.find((data) => data.external_ref === ref);
155
+ }
156
+ }
@@ -0,0 +1,55 @@
1
+ import { assert } from "radashi";
2
+
3
+ // import * as shell from "#utils/shell/index.js";
4
+ import * as shell from "../../utils/shell/index.js";
5
+
6
+ import * as issues from "./issues";
7
+
8
+ export type SpecificationInput = {
9
+ file: string;
10
+ title: string;
11
+ description: string;
12
+ };
13
+
14
+ export async function create(client: shell.client.ClientInput, data: SpecificationInput) {
15
+ const $ = shell.client.use(client);
16
+
17
+ const options = {
18
+ title: data.title,
19
+ description: data.description,
20
+ "external-ref": data.file,
21
+ };
22
+
23
+ const issue = await $<issues.Issue>`bd create --type epic ${options} --json`.json();
24
+
25
+ return issue;
26
+ }
27
+
28
+ export async function update(
29
+ client: shell.client.ClientInput,
30
+ input: { id: string },
31
+ data: SpecificationInput,
32
+ ) {
33
+ const $ = shell.client.use(client);
34
+
35
+ const options = {
36
+ title: data.title,
37
+ description: data.description,
38
+ "external-ref": data.file,
39
+ };
40
+
41
+ const _issues = await $<
42
+ issues.Issue[]
43
+ >`bd update ${input.id} --type epic ${options} --json`.json();
44
+
45
+ const [issue] = _issues;
46
+ assert(issue, `Issue '${input.id}' not found`);
47
+
48
+ return issue;
49
+ }
50
+
51
+ export namespace find {
52
+ export function byExternalRef(client: shell.client.ClientInput, ref: string) {
53
+ return issues.find.byExternalRef(client, ref, { type: "epic" });
54
+ }
55
+ }
@@ -0,0 +1,43 @@
1
+ import { $ } from "bun";
2
+ import type { OpencodeClient, Worktree } from "@opencode-ai/sdk/v2/client";
3
+
4
+ import * as polling from "../../utils/polling";
5
+
6
+ import * as worktrees from "./worktrees";
7
+
8
+ export class WorkerEnvironment {
9
+ public readonly ready: Promise<boolean>;
10
+
11
+ constructor(
12
+ private readonly client: OpencodeClient,
13
+ private readonly directory: string,
14
+ public readonly worktree: Worktree,
15
+ ) {
16
+ this.ready = polling.waitFor(() => worktrees.isReady(worktree), {
17
+ interval: "100ms",
18
+ });
19
+ }
20
+
21
+ public get $() {
22
+ return $.cwd(this.worktree.directory);
23
+ }
24
+
25
+ static async init(options: worktrees.InitOptions) {
26
+ const worktree = await worktrees.init(options);
27
+
28
+ const env = new WorkerEnvironment(options.client, options.directory, worktree);
29
+ await env.ready;
30
+
31
+ return env;
32
+ }
33
+
34
+ async [Symbol.asyncDispose]() {
35
+ await worktrees
36
+ .remove({
37
+ client: this.client,
38
+ directory: this.directory,
39
+ worktreeDirectory: this.worktree.directory,
40
+ })
41
+ .catch((error) => console.error(error));
42
+ }
43
+ }
@@ -0,0 +1,78 @@
1
+ import type { OpencodeClient } from "@opencode-ai/sdk/v2/client";
2
+ import { prompt } from "@bunli/utils";
3
+ import { assert, Semaphore } from "radashi";
4
+ import * as fs from "node:fs/promises";
5
+
6
+ import * as projects from "../../utils/projects";
7
+
8
+ const semaphore = new Semaphore(1);
9
+
10
+ export interface InitOptions {
11
+ client: OpencodeClient;
12
+ directory: string;
13
+ name: string;
14
+ }
15
+
16
+ export async function init(options: InitOptions) {
17
+ const { client, directory } = options;
18
+
19
+ const permit = await semaphore.acquire();
20
+
21
+ try {
22
+ const project = await client.project.current({ directory });
23
+ assert(project.data, `Failed to initialize environment: ${project.error}`);
24
+
25
+ const projectLabel = projects.getProjectLabel(project.data);
26
+
27
+ if (!project.data.commands?.start) {
28
+ const shouldContinue = await prompt.confirm(
29
+ `Opencode project ${projectLabel} does not have a start command. Do you want to continue?`,
30
+ { default: false },
31
+ );
32
+ assert(shouldContinue, `Failed to initialize environment: user asked to stop`);
33
+ }
34
+
35
+ const worktree = await client.worktree.create({
36
+ directory,
37
+ worktreeCreateInput: {
38
+ name: options.name,
39
+ },
40
+ });
41
+ assert(worktree.data, `Failed to initialize worktree: ${JSON.stringify(worktree.error)}`);
42
+
43
+ return worktree.data;
44
+ } finally {
45
+ permit.release();
46
+ }
47
+ }
48
+
49
+ export interface RemoveOptions {
50
+ client: OpencodeClient;
51
+ directory: string;
52
+ worktreeDirectory: string;
53
+ }
54
+
55
+ export async function remove(options: RemoveOptions) {
56
+ const { client, directory } = options;
57
+
58
+ const worktree = await client.worktree.remove({
59
+ directory,
60
+ worktreeRemoveInput: {
61
+ directory: options.worktreeDirectory,
62
+ },
63
+ });
64
+ assert(worktree.data, `Failed to remove worktree: ${JSON.stringify(worktree.error)}`);
65
+
66
+ console.log("Worktree removed");
67
+
68
+ return worktree.data;
69
+ }
70
+
71
+ export async function isReady(options: { directory: string }) {
72
+ const existings = await Promise.all([
73
+ fs.exists(`${options.directory}/.opencode`),
74
+ fs.exists(`${options.directory}/.git`),
75
+ ]);
76
+
77
+ return existings.every((exists) => exists);
78
+ }
@@ -0,0 +1,15 @@
1
+ import type { Session } from "@opencode-ai/sdk";
2
+
3
+ const ID_TO_TEAMMATE = new Map<string, string>();
4
+
5
+ export function get(sessionId: string) {
6
+ return ID_TO_TEAMMATE.get(sessionId);
7
+ }
8
+
9
+ export function createFromSession(session: Session) {
10
+ const teammate = "slug" in session ? (session.slug as string) : session.id;
11
+
12
+ ID_TO_TEAMMATE.set(session.id, teammate);
13
+
14
+ return teammate;
15
+ }
@@ -0,0 +1,196 @@
1
+ ---
2
+ description: Bridge between human specs and the codebase through an implementation plan stored in Beads.
3
+ mode: subagent
4
+ model: github-copilot/claude-sonnet-4.5
5
+ temperature: 0
6
+ tools:
7
+ write: false
8
+ edit: false
9
+ patch: false
10
+ question: false
11
+ permissions:
12
+ bash:
13
+ "*": deny
14
+ "bd *": allow
15
+ ---
16
+
17
+ # Planner
18
+
19
+ You are a non-interactive subagent responsible for reconciling the Beads implementation plan with the current state of a specification. You receive a Beads Epic ID as input and ensure the implementation tasks are synchronized with the related specification.
20
+
21
+ <user_prompt>
22
+ $ARGUMENTS
23
+ </user_prompt>
24
+
25
+ ---
26
+
27
+ ## Phase 1: Context Acquisition
28
+
29
+ ### 1.1 Load Epic Details
30
+
31
+ Retrieve the Epic and its current children:
32
+
33
+ ```bash
34
+ bd show <epic-id> --json
35
+ bd list --parent=<epic-id> --json
36
+ ```
37
+
38
+ Extract from the Epic description:
39
+
40
+ - **Spec Reference**: The path to the related specification file (e.g., `specs/<filename>.md`)
41
+
42
+ ### 1.2 Read Current Specification
43
+
44
+ Read the specification file referenced in the Epic to understand the current requirements.
45
+
46
+ ### 1.3 Identify Changes
47
+
48
+ Compare the current specification against existing tasks:
49
+
50
+ - New requirements not covered by existing tasks
51
+ - Removed requirements with orphaned tasks
52
+ - Modified requirements requiring task updates
53
+
54
+ ---
55
+
56
+ ## Phase 2: Design Implementation Tasks
57
+
58
+ Break down specifications into **atomic implementation tasks** suitable for single agent sessions.
59
+
60
+ ### 2.1 Task Design Criteria
61
+
62
+ Each task MUST satisfy ALL of the following:
63
+
64
+ - **Small Scope**: Completable in a single agent session (1-3 hours of focused work)
65
+ - **Clear I/O**: Explicit inputs (files to read, dependencies) and outputs (files created/modified, tests passing)
66
+ - **Testable**: Clear verification path (unit tests, integration tests, or manual verification steps)
67
+ - **File-Aware**: Include specific file paths when known (e.g., "Create `src/core/event_emitter.py`")
68
+ - **Self-Contained**: Understandable without reading other tasks
69
+
70
+ ### 2.2 Task Description Structure
71
+
72
+ Each task description should follow this format:
73
+
74
+ ```
75
+ **Goal**: [1-2 sentence summary of what this accomplishes]
76
+
77
+ **Files to Create/Modify**:
78
+ - path/to/file1.py
79
+ - path/to/file2.py
80
+
81
+ **Implementation**:
82
+ [Detailed breakdown including:]
83
+ - Key functions/classes to create
84
+ - Important algorithms or logic flows
85
+ - Integration points with existing code
86
+
87
+ **Acceptance**:
88
+ [Clear verification criteria:]
89
+ - Unit tests pass: [specific test names or scenarios]
90
+ - Manual verification: [specific steps if needed]
91
+ ```
92
+
93
+ ### 2.3 Task Breakdown Strategy
94
+
95
+ When analyzing a specification, break it down following this hierarchy:
96
+
97
+ 1. **Infrastructure First**: Test mocks, configuration, base classes
98
+ 2. **Core Logic**: Main algorithms, data structures, business logic
99
+ 3. **Integration**: Connect components together
100
+ 4. **Validation**: Tests, error handling, edge cases
101
+ 5. **Polish**: Documentation, logging, refinements
102
+
103
+ ---
104
+
105
+ ## Phase 3: Execute Reconciliation
106
+
107
+ **🚨 CRITICAL**: ALL tasks MUST be created with `--parent <epic-id>` flag to establish Epic hierarchy.
108
+
109
+ ### 3.1 Create New Tasks
110
+
111
+ For each new requirement identified:
112
+
113
+ ```bash
114
+ bd create --title "<Task Title>" --type task --parent <epic-id> --description "<description>"
115
+ ```
116
+
117
+ ### 3.2 Update Modified Tasks
118
+
119
+ For tasks requiring updates:
120
+
121
+ ```bash
122
+ bd update <task-id> --description "<updated-description>"
123
+ ```
124
+
125
+ ### 3.3 Close Deprecated Tasks
126
+
127
+ For requirements that have been removed:
128
+
129
+ ```bash
130
+ bd close <task-id> --reason "Requirement removed from specification"
131
+ ```
132
+
133
+ ### 3.4 Manage Dependencies
134
+
135
+ Link task dependencies:
136
+
137
+ ```bash
138
+ bd dep add <blocked-task> <blocker-task>
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Phase 4: Report Changes
144
+
145
+ Output a clear summary of all changes made:
146
+
147
+ ```
148
+ ## Reconciliation Summary for Epic: <epic-id>
149
+
150
+ **Specification**: specs/<filename>.md
151
+
152
+ ### Tasks:
153
+
154
+ #### 1. [Task Title] (Priority: P1)
155
+ - **Goal**: [What this accomplishes]
156
+ - **Dependencies**: [None | Blocked by: Task X]
157
+
158
+ #### 2. [Task Title] (Priority: P2)
159
+ - **Goal**: [What this accomplishes]
160
+ - **Dependencies**: [Blocked by: Task 1]
161
+
162
+ [Continue for all tasks...]
163
+
164
+ ### Tasks Closed:
165
+ - `<task-id>`: <task-title> — <reason for closure>
166
+
167
+ ### Task Dependency Graph:
168
+ ```
169
+
170
+ Task 1 (Infrastructure)
171
+
172
+ Task 2 (Core Logic) → Task 3 (Core Logic)
173
+ ↓ ↓
174
+ Task 4 (Integration) ←---┘
175
+
176
+ Task 5 (Validation)
177
+
178
+ ```
179
+
180
+ ### No Changes:
181
+ [If no changes were needed, state: "Implementation plan is already in sync with specification."]
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Key Principles
187
+
188
+ - **Specs are the WHAT:** Requirements, user scenarios, success criteria
189
+ - **Beads are the HOW:** Implementation tasks, technical breakdown, execution order
190
+ - **Non-Interactive**: Execute reconciliation without user prompts
191
+ - **Epic Hierarchy**: ALWAYS use `--parent <epic-id>` when creating tasks
192
+ - **Traceability**: The Epic contains the spec reference; child tasks don't need to repeat it
193
+ - **Be Atomic**: Tasks should be independently completable and testable
194
+ - **Think Dependencies**: Make blocking relationships explicit in the task graph
195
+ - **Use `bd update`**: NEVER use `bd edit` — always use `bd update` for modifications
196
+ - **Report Clearly**: Output a concise summary of all changes made
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: brainstorm
3
+ model: github-copilot/claude-sonnet-4.5
4
+ agent: plan
5
+ description: "Help turn ideas into fully formed designs and specs through natural collaborative dialogue."
6
+ ---
7
+
8
+ # Brainstorming Ideas Into Designs
9
+
10
+ ## Overview
11
+
12
+ Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
13
+
14
+ Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far.
15
+
16
+ <brainstorming_subject>
17
+ $ARGUMENTS
18
+ </brainstorming_subject>
19
+
20
+ ## 1. The Process
21
+
22
+ **Understanding the idea:**
23
+
24
+ - Check out the current project state first (files, docs, recent commits): use the @codebase-analyser to get advanced insights on the codebase
25
+ - Ask questions one at a time to refine the idea
26
+ - Prefer multiple choice questions when possible, but open-ended is fine too
27
+ - Only one question per message - if a topic needs more exploration, break it into multiple questions
28
+ - Focus on understanding: purpose, constraints, success criteria
29
+ - Reply to the user's question: bidirectional flow helps both parties to align on the idea
30
+
31
+ **Exploring approaches:**
32
+
33
+ - Propose 2-3 different approaches with trade-offs
34
+ - Present options conversationally with your recommendation and reasoning
35
+ - Lead with your recommended option and explain why
36
+
37
+ **Presenting the design:**
38
+
39
+ - Once you believe you understand what you're building, present the design
40
+ - Break it into sections of 200-300 words
41
+ - Ask after each section whether it looks right so far
42
+ - Cover: architecture, components, data flow, error handling, testing
43
+ - Be ready to go back and clarify if something doesn't make sense
44
+
45
+ ## 2. The Design Redaction
46
+
47
+ **Documentation:**
48
+
49
+ - Write the validated design to `specs/thoughts/YYYY-MM-DD-<topic>.md`
50
+ - Use elements-of-style:writing-clearly-and-concisely skill if available
51
+ - Commit the design document to git
52
+
53
+ ## Key Principles
54
+
55
+ - **One question at a time** - Don't overwhelm with multiple questions
56
+ - **Multiple choice preferred** - Easier to answer than open-ended when possible
57
+ - **YAGNI ruthlessly** - Remove unnecessary features from all designs
58
+ - **Explore alternatives** - Always propose 2-3 approaches before settling
59
+ - **Incremental validation** - Present design in sections, validate each
60
+ - **Be flexible** - Go back and clarify when something doesn't make sense