claude-teammate 0.1.2 → 0.1.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/README.md CHANGED
@@ -85,7 +85,7 @@ memory/
85
85
  | `repos` | Target GitHub repositories |
86
86
  | `status` | Done, in-flight, blocked |
87
87
  | `notes` | Architectural decisions, reviewer preferences, conventions |
88
- | `known issues` | Recurring bugs, fragile areas, patterns to avoid |
88
+ | `guardrails` | Recurring bugs, fragile areas, and what not to do |
89
89
 
90
90
  The bot carries context across tickets, PRs, and sessions.
91
91
 
@@ -100,7 +100,7 @@ npm install -g claude-teammate
100
100
  claude-teammate start
101
101
  ```
102
102
 
103
- The wizard asks for your Jira and GitHub credentials and writes a `.env` file in the current directory.
103
+ `start` creates `.env` on the first run, runs a 5 second Claude CLI preflight check that expects a plain `OK`, then launches a background worker for the current project. If `.env` already exists with the required values, setup is skipped and your credentials are not prompted for again.
104
104
 
105
105
  To upgrade to latest version
106
106
 
@@ -108,12 +108,42 @@ To upgrade to latest version
108
108
  npm install -g claude-teammate@latest
109
109
  ```
110
110
 
111
+ Check or stop the worker with:
112
+
113
+ ```bash
114
+ claude-teammate status
115
+ claude-teammate stop
116
+ ```
117
+
118
+ Per-project runtime files are stored in:
119
+
120
+ ```text
121
+ .claude-teammate/
122
+ ├── repos/
123
+ ├── worker.log
124
+ ├── worker.pid
125
+ └── state.json
126
+ ```
127
+
128
+ Issue memory is stored in:
129
+
130
+ ```text
131
+ memory/
132
+ └── {domain}/
133
+ └── {workspace}/
134
+ ├── epic-{id}.md
135
+ └── issue-{id}.md
136
+ ```
137
+
138
+ `epic-{id}.md` stores shared epic facts such as related repositories. `issue-{id}.md` stores issue-specific workflow state, clarification history, and GitHub issue tracking.
139
+
140
+ The worker polls Jira once per minute. It transitions `To Do` issues to `In Progress`, asks for repo URLs when epic memory lacks them, clones all referenced repos into `.claude-teammate/repos/`, uses Claude CLI to clarify requirements from Jira comments across those repos, and creates GitHub issues once the requirements are clear enough.
141
+
111
142
  ```env
112
143
  # .env file generated by `claude-teammate start`
113
144
  JIRA_BASE_URL=https://yourorg.atlassian.net
114
- JIRA_EMAIL=you@example.com
115
- JIRA_API_TOKEN=...
116
145
  JIRA_BOT_EMAIL=bot@yourorg.com
146
+ JIRA_BOT_API_TOKEN=...
117
147
  GITHUB_PAT=ghp_...
118
148
  ```
119
149
 
@@ -138,7 +168,7 @@ It reads the actual repository before writing anything - structure, conventions,
138
168
  ## Roadmap
139
169
 
140
170
  - 🟢 Design the workflow
141
- - Jira integration - read tickets, comment, detect assignee
171
+ - 🟢 Jira integration - read tickets, comment, detect assignee
142
172
  - ⚪ GitHub integration - issues, PRs, review requests
143
173
  - ⚪ Epic memory - persistent context per epic
144
174
  - ⚪ End-to-end: Jira ticket → approved plan → merged PR
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "scripts": {
19
19
  "lint": "eslint .",
20
- "start": "node ./bin/claude-teammate.js start"
20
+ "start": "node ./bin/claude-teammate.js start",
21
+ "test": "node --test"
21
22
  },
22
23
  "keywords": [
23
24
  "cli",
package/src/claude.js ADDED
@@ -0,0 +1,356 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
4
+ const STARTUP_CHECK_TIMEOUT_MS = 15 * 1000;
5
+ const CHILD_CLEANUP_WAIT_MS = 1_000;
6
+ const activeClaudeChildren = new Set();
7
+ const DEFAULT_MODEL = "sonnet";
8
+
9
+ const OUTPUT_SCHEMA = {
10
+ type: "object",
11
+ additionalProperties: false,
12
+ properties: {
13
+ decision: {
14
+ type: "string",
15
+ enum: ["needs_clarification", "requirements_clear"]
16
+ },
17
+ clarification_summary: {
18
+ type: "string"
19
+ },
20
+ questions: {
21
+ type: "array",
22
+ items: {
23
+ type: "string"
24
+ }
25
+ },
26
+ plan_title: {
27
+ type: "string"
28
+ },
29
+ plan_body: {
30
+ type: "string"
31
+ }
32
+ },
33
+ required: ["decision", "clarification_summary", "questions", "plan_title", "plan_body"]
34
+ };
35
+
36
+ export async function runClaudeClarification(input) {
37
+ if (!Array.isArray(input.repoPaths) || input.repoPaths.length === 0) {
38
+ throw new Error("Claude clarification requires at least one local repository path.");
39
+ }
40
+
41
+ const args = [
42
+ "--print",
43
+ "--model",
44
+ input.model || DEFAULT_MODEL,
45
+ "--permission-mode",
46
+ input.permissionMode || "dontAsk",
47
+ "--strict-mcp-config",
48
+ "--tools",
49
+ "Read,Grep,Glob",
50
+ "--effort",
51
+ "low",
52
+ "--output-format",
53
+ "json",
54
+ "--json-schema",
55
+ JSON.stringify(OUTPUT_SCHEMA),
56
+ "--append-system-prompt",
57
+ buildSystemPrompt()
58
+ ];
59
+
60
+ args.push(
61
+ buildUserPrompt(input)
62
+ );
63
+
64
+ let stdout;
65
+ const workingDirectory = getClaudeWorkspaceRoot(input.repoPaths);
66
+
67
+ try {
68
+ ({ stdout } = await runClaudeCommand("claude", args, {
69
+ cwd: workingDirectory,
70
+ maxBuffer: 10 * 1024 * 1024,
71
+ timeout: input.timeoutMs || DEFAULT_TIMEOUT_MS
72
+ }));
73
+ } catch (error) {
74
+ throw new Error(formatClaudeInvocationError(error, input.timeoutMs || DEFAULT_TIMEOUT_MS));
75
+ }
76
+
77
+ const parsed = parseClaudeOutput(stdout);
78
+ return validateClaudeResult(parsed);
79
+ }
80
+
81
+ export async function verifyClaudeCli(input = {}) {
82
+ const args = [
83
+ "--print",
84
+ "--output-format",
85
+ "text",
86
+ "Reply with exactly OK. Output only OK."
87
+ ];
88
+
89
+ let stdout;
90
+
91
+ try {
92
+ ({ stdout } = await runClaudeCommand("claude", args, {
93
+ cwd: input.cwd,
94
+ maxBuffer: 1024 * 1024,
95
+ timeout: input.timeoutMs || STARTUP_CHECK_TIMEOUT_MS
96
+ }));
97
+ } catch (error) {
98
+ throw new Error(formatClaudeInvocationError(error, input.timeoutMs || STARTUP_CHECK_TIMEOUT_MS));
99
+ }
100
+
101
+ const normalizedOutput = stdout.trim().replace(/[.!]+$/u, "");
102
+ if (normalizedOutput !== "OK") {
103
+ throw new Error(`Claude CLI startup check returned unexpected output: ${stdout.trim() || "(empty)"}`);
104
+ }
105
+ }
106
+
107
+ export function parseClaudeOutput(output) {
108
+ const trimmed = output.trim();
109
+ if (!trimmed) {
110
+ throw new Error("Claude CLI returned empty output.");
111
+ }
112
+
113
+ const direct = JSON.parse(trimmed);
114
+ if (direct && typeof direct === "object") {
115
+ if ("structured_output" in direct && direct.structured_output && typeof direct.structured_output === "object") {
116
+ return direct.structured_output;
117
+ }
118
+
119
+ if ("decision" in direct) {
120
+ return direct;
121
+ }
122
+
123
+ if ("result" in direct && typeof direct.result === "string") {
124
+ return JSON.parse(direct.result);
125
+ }
126
+
127
+ if ("content" in direct && typeof direct.content === "string") {
128
+ return JSON.parse(direct.content);
129
+ }
130
+ }
131
+
132
+ throw new Error("Claude CLI returned an unsupported JSON payload.");
133
+ }
134
+
135
+ export async function cleanupClaudeProcesses() {
136
+ const children = [...activeClaudeChildren];
137
+ if (children.length === 0) {
138
+ return;
139
+ }
140
+
141
+ await Promise.all(children.map((child) => terminateChildProcess(child)));
142
+ }
143
+
144
+ function validateClaudeResult(result) {
145
+ if (
146
+ !result ||
147
+ typeof result !== "object" ||
148
+ !["needs_clarification", "requirements_clear"].includes(result.decision)
149
+ ) {
150
+ throw new Error("Claude CLI returned an invalid decision payload.");
151
+ }
152
+
153
+ return {
154
+ decision: result.decision,
155
+ clarification_summary: String(result.clarification_summary ?? "").trim(),
156
+ questions: Array.isArray(result.questions)
157
+ ? result.questions.map((question) => String(question).trim()).filter(Boolean)
158
+ : [],
159
+ plan_title: String(result.plan_title ?? "").trim(),
160
+ plan_body: String(result.plan_body ?? "").trim()
161
+ };
162
+ }
163
+
164
+ function formatClaudeInvocationError(error, timeoutMs) {
165
+ const stderr = error instanceof Error && "stderr" in error ? String(error.stderr || "") : "";
166
+ const output = error instanceof Error && "stdout" in error ? String(error.stdout || "") : "";
167
+ const timeout = Boolean(error && typeof error === "object" && "killed" in error && error.killed);
168
+ const signal = error && typeof error === "object" && "signal" in error ? String(error.signal || "") : "";
169
+ const details = [stderr.trim(), output.trim()].filter(Boolean).join("\n").slice(0, 1000);
170
+ return `Claude CLI invocation failed${timeout ? ` after ${timeoutMs}ms` : ""}${signal ? ` (${signal})` : ""}${details ? `: ${details}` : "."}`;
171
+ }
172
+
173
+ function runClaudeCommand(command, args, options) {
174
+ return new Promise((resolve, reject) => {
175
+ const child = spawn(command, args, {
176
+ cwd: options.cwd,
177
+ stdio: ["ignore", "pipe", "pipe"]
178
+ });
179
+ activeClaudeChildren.add(child);
180
+
181
+ let stdout = "";
182
+ let stderr = "";
183
+ let settled = false;
184
+ let timedOut = false;
185
+ let killedBySignal = "";
186
+
187
+ const timer = setTimeout(() => {
188
+ timedOut = true;
189
+ child.kill("SIGTERM");
190
+ }, options.timeout);
191
+
192
+ child.stdout.setEncoding("utf8");
193
+ child.stderr.setEncoding("utf8");
194
+
195
+ child.stdout.on("data", (chunk) => {
196
+ stdout += chunk;
197
+ if (stdout.length > options.maxBuffer) {
198
+ killedBySignal = "SIGTERM";
199
+ child.kill("SIGTERM");
200
+ }
201
+ });
202
+
203
+ child.stderr.on("data", (chunk) => {
204
+ stderr += chunk;
205
+ if (stderr.length > options.maxBuffer) {
206
+ killedBySignal = "SIGTERM";
207
+ child.kill("SIGTERM");
208
+ }
209
+ });
210
+
211
+ child.on("error", (error) => {
212
+ if (settled) {
213
+ return;
214
+ }
215
+ settled = true;
216
+ activeClaudeChildren.delete(child);
217
+ clearTimeout(timer);
218
+ reject({
219
+ ...error,
220
+ stdout,
221
+ stderr,
222
+ killed: timedOut,
223
+ signal: killedBySignal
224
+ });
225
+ });
226
+
227
+ child.on("close", (code, signal) => {
228
+ if (settled) {
229
+ return;
230
+ }
231
+ settled = true;
232
+ activeClaudeChildren.delete(child);
233
+ clearTimeout(timer);
234
+
235
+ if (code === 0) {
236
+ resolve({ stdout, stderr });
237
+ return;
238
+ }
239
+
240
+ reject({
241
+ stdout,
242
+ stderr,
243
+ killed: timedOut,
244
+ signal: signal || killedBySignal
245
+ });
246
+ });
247
+ });
248
+ }
249
+
250
+ function terminateChildProcess(child) {
251
+ return new Promise((resolve) => {
252
+ if (child.exitCode !== null || child.killed) {
253
+ activeClaudeChildren.delete(child);
254
+ resolve();
255
+ return;
256
+ }
257
+
258
+ let finished = false;
259
+ const finish = () => {
260
+ if (finished) {
261
+ return;
262
+ }
263
+ finished = true;
264
+ activeClaudeChildren.delete(child);
265
+ resolve();
266
+ };
267
+
268
+ child.once("close", finish);
269
+ child.kill("SIGTERM");
270
+
271
+ setTimeout(() => {
272
+ if (finished) {
273
+ return;
274
+ }
275
+ child.kill("SIGKILL");
276
+ finish();
277
+ }, CHILD_CLEANUP_WAIT_MS);
278
+ });
279
+ }
280
+
281
+ function buildSystemPrompt() {
282
+ return [
283
+ "You are clarifying a Jira ticket for an implementation planning workflow.",
284
+ "You may inspect the provided repository if that helps determine whether the requirement is clear enough or helps write a better implementation plan.",
285
+ "Use tools selectively; only inspect code when it materially helps the decision or plan.",
286
+ "Do not edit files, do not run mutating commands, and do not implement anything.",
287
+ "Return only structured output that matches the provided schema.",
288
+ "If requirements are unclear, ask concise clarification questions for the Jira user.",
289
+ "If requirements are clear, produce a concise implementation plan title and body suitable for a GitHub issue.",
290
+ "The plan_title must use exactly this format: <type>: <title> [issue-id].",
291
+ "Allowed types are only: feat, fix, docs, style, refactor, test, chore.",
292
+ "Use the provided Jira issue key as the issue-id suffix."
293
+ ].join(" ");
294
+ }
295
+
296
+ function buildUserPrompt(input) {
297
+ const recentComments = input.comments
298
+ .slice(-10)
299
+ .map(
300
+ (comment) =>
301
+ `- [${comment.id}] ${comment.author.displayName || comment.author.emailAddress || "Unknown"}: ${comment.bodyText}`
302
+ )
303
+ .join("\n");
304
+
305
+ return `Clarify this Jira issue.
306
+
307
+ Issue key: ${input.issue.key}
308
+ Issue URL: ${input.issue.url}
309
+ Summary: ${input.issue.summary}
310
+ Status: ${input.issue.status}
311
+ Description:
312
+ ${input.issue.descriptionText || "(none)"}
313
+
314
+ Memory snapshot:
315
+ ${JSON.stringify(input.memory, null, 2)}
316
+
317
+ Recent Jira comments:
318
+ ${recentComments || "(none)"}
319
+
320
+ Repository path:
321
+ ${input.repoPaths.join("\n")}
322
+
323
+ Decide whether the requirements are clear enough to plan implementation. Read code only if it helps.`;
324
+ }
325
+
326
+ function getClaudeWorkspaceRoot(repoPaths) {
327
+ if (repoPaths.length === 1) {
328
+ return repoPaths[0];
329
+ }
330
+
331
+ let current = repoPaths[0];
332
+ for (const repoPath of repoPaths.slice(1)) {
333
+ current = commonAncestorPath(current, repoPath);
334
+ }
335
+ return current;
336
+ }
337
+
338
+ function commonAncestorPath(left, right) {
339
+ const leftParts = path.resolve(left).split(path.sep);
340
+ const rightParts = path.resolve(right).split(path.sep);
341
+ const shared = [];
342
+ const maxLength = Math.min(leftParts.length, rightParts.length);
343
+
344
+ for (let index = 0; index < maxLength; index += 1) {
345
+ if (leftParts[index] !== rightParts[index]) {
346
+ break;
347
+ }
348
+ shared.push(leftParts[index]);
349
+ }
350
+
351
+ if (shared.length === 0) {
352
+ return path.parse(path.resolve(left)).root;
353
+ }
354
+
355
+ return shared.join(path.sep) || path.sep;
356
+ }
package/src/cli.js CHANGED
@@ -1,19 +1,28 @@
1
1
  import process from "node:process";
2
2
 
3
- import { runStartWizard } from "./commands/start.js";
3
+ import { runStartCommand } from "./commands/start.js";
4
+ import { runStatusCommand } from "./commands/status.js";
5
+ import { runStopCommand } from "./commands/stop.js";
6
+ import { runWorkerCommand } from "./commands/worker.js";
4
7
 
5
8
  const HELP_TEXT = `claude-teammate
6
9
 
7
10
  Usage:
8
11
  claude-teammate start
12
+ claude-teammate stop
13
+ claude-teammate status
9
14
  claude-teammate --help
10
15
 
11
16
  Commands:
12
- start Run the setup wizard and write configuration to .env
17
+ start Start Claude Teammate in the background for this project
18
+ stop Stop the background worker for this project
19
+ status Show worker status, logs, and last Jira poll state
13
20
  `;
14
21
 
15
22
  export async function runCli(args) {
16
23
  const [command] = args;
24
+ const projectRoot = process.cwd();
25
+ const entrypointPath = process.argv[1];
17
26
 
18
27
  if (!command || command === "--help" || command === "-h") {
19
28
  process.stdout.write(`${HELP_TEXT}\n`);
@@ -21,7 +30,27 @@ export async function runCli(args) {
21
30
  }
22
31
 
23
32
  if (command === "start") {
24
- await runStartWizard();
33
+ await runStartCommand({ projectRoot, entrypointPath });
34
+ return;
35
+ }
36
+
37
+ if (command === "stop") {
38
+ await runStopCommand({ projectRoot });
39
+ return;
40
+ }
41
+
42
+ if (command === "status") {
43
+ await runStatusCommand({ projectRoot });
44
+ return;
45
+ }
46
+
47
+ if (command === "run-worker") {
48
+ const workerProjectRoot = args[1];
49
+ if (!workerProjectRoot) {
50
+ throw new Error("Missing project root for run-worker.");
51
+ }
52
+
53
+ await runWorkerCommand({ projectRoot: workerProjectRoot });
25
54
  return;
26
55
  }
27
56