claude-teammate 0.1.63 → 0.1.65

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-teammate",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "description": "CLI bootstrapper for Claude Teammate.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/claude.js CHANGED
@@ -192,6 +192,45 @@ const EPIC_MEMORY_CLEANUP_SCHEMA = {
192
192
  required: ["facts", "guardrails"]
193
193
  };
194
194
 
195
+ const CODE_CHANGE_DECISION_SCHEMA = {
196
+ type: "object",
197
+ additionalProperties: false,
198
+ properties: {
199
+ verdict: {
200
+ type: "string",
201
+ enum: ["needs_code", "no_code"]
202
+ }
203
+ },
204
+ required: ["verdict"]
205
+ };
206
+
207
+ const NO_CODE_TASK_SCHEMA = {
208
+ type: "object",
209
+ additionalProperties: false,
210
+ properties: {
211
+ result: {
212
+ type: "string",
213
+ enum: ["completed", "failed"]
214
+ },
215
+ summary: {
216
+ type: "string"
217
+ },
218
+ epic_facts: {
219
+ type: "array",
220
+ items: {
221
+ type: "string"
222
+ }
223
+ },
224
+ epic_guardrails: {
225
+ type: "array",
226
+ items: {
227
+ type: "string"
228
+ }
229
+ }
230
+ },
231
+ required: ["result", "summary", "epic_facts", "epic_guardrails"]
232
+ };
233
+
195
234
  export async function runClaudeClarification(input) {
196
235
  if (!Array.isArray(input.repoPaths) || input.repoPaths.length === 0) {
197
236
  throw new Error("Claude clarification requires at least one local repository path.");
@@ -451,6 +490,84 @@ export async function runClaudeEpicMemorySummarize(input) {
451
490
  return validateEpicMemoryCleanupResult(parseClaudeOutput(stdout));
452
491
  }
453
492
 
493
+ export async function runClaudeCodeChangeDecision(input) {
494
+ if (!Array.isArray(input.repoPaths) || input.repoPaths.length === 0) {
495
+ throw new Error("Claude code change decision requires at least one local repository path.");
496
+ }
497
+
498
+ const args = [
499
+ "--print",
500
+ "--model",
501
+ input.model || DEFAULT_MODEL,
502
+ "--permission-mode",
503
+ resolveClaudePermissionMode(input.permissionMode),
504
+ "--effort",
505
+ "low",
506
+ "--output-format",
507
+ "json",
508
+ "--json-schema",
509
+ JSON.stringify(CODE_CHANGE_DECISION_SCHEMA),
510
+ "--append-system-prompt",
511
+ buildCodeChangeDecisionSystemPrompt(),
512
+ buildCodeChangeDecisionUserPrompt(input)
513
+ ];
514
+
515
+ let stdout;
516
+ const workingDirectory = getClaudeWorkspaceRoot(input.repoPaths);
517
+
518
+ try {
519
+ ({ stdout } = await runClaudeCommand("claude", args, {
520
+ cwd: workingDirectory,
521
+ maxBuffer: 10 * 1024 * 1024,
522
+ timeout: input.timeoutMs || DEFAULT_TIMEOUT_MS
523
+ }));
524
+ } catch (error) {
525
+ throw new Error(formatClaudeInvocationError(error, input.timeoutMs || DEFAULT_TIMEOUT_MS));
526
+ }
527
+
528
+ return validateCodeChangeDecisionResult(parseClaudeOutput(stdout));
529
+ }
530
+
531
+ export async function runClaudeNoCodeTask(input) {
532
+ if (!Array.isArray(input.repoPaths) || input.repoPaths.length === 0) {
533
+ throw new Error("Claude no-code task requires at least one local repository path.");
534
+ }
535
+
536
+ const repoPaths = [...new Set(input.repoPaths.map((repoPath) => path.resolve(repoPath)))];
537
+
538
+ const args = [
539
+ "--print",
540
+ "--model",
541
+ input.model || DEFAULT_MODEL,
542
+ "--permission-mode",
543
+ resolveClaudePermissionMode(input.permissionMode),
544
+ "--effort",
545
+ "medium",
546
+ "--output-format",
547
+ "json",
548
+ "--json-schema",
549
+ JSON.stringify(NO_CODE_TASK_SCHEMA),
550
+ "--append-system-prompt",
551
+ buildNoCodeTaskSystemPrompt(),
552
+ buildNoCodeTaskUserPrompt(input)
553
+ ];
554
+
555
+ let stdout;
556
+
557
+ try {
558
+ ({ stdout } = await runClaudeCommand("claude", args, {
559
+ cwd: getClaudeWorkspaceRoot(repoPaths),
560
+ maxBuffer: 10 * 1024 * 1024,
561
+ timeout: input.timeoutMs || 30 * 60 * 1000,
562
+ onSpawn: input.onSpawn
563
+ }));
564
+ } catch (error) {
565
+ throw new Error(formatClaudeInvocationError(error, input.timeoutMs || 30 * 60 * 1000));
566
+ }
567
+
568
+ return validateNoCodeTaskResult(parseClaudeOutput(stdout));
569
+ }
570
+
454
571
  export async function runClaudeImplementation(input) {
455
572
  if (!input.repoPath) {
456
573
  throw new Error("Claude implementation requires a local repository path.");
@@ -605,6 +722,14 @@ export function parseClaudeOutput(output) {
605
722
  return direct;
606
723
  }
607
724
 
725
+ if ("verdict" in direct) {
726
+ return direct;
727
+ }
728
+
729
+ if ("result" in direct && "summary" in direct) {
730
+ return direct;
731
+ }
732
+
608
733
  if ("result" in direct && typeof direct.result === "string") {
609
734
  return JSON.parse(direct.result);
610
735
  }
@@ -753,6 +878,35 @@ function validateEpicMemoryCleanupResult(result) {
753
878
  };
754
879
  }
755
880
 
881
+ function validateCodeChangeDecisionResult(result) {
882
+ if (!result || typeof result !== "object" || !["needs_code", "no_code"].includes(result.verdict)) {
883
+ throw new Error("Claude CLI returned an invalid code change decision payload.");
884
+ }
885
+
886
+ return { verdict: result.verdict };
887
+ }
888
+
889
+ function validateNoCodeTaskResult(result) {
890
+ if (!result || typeof result !== "object") {
891
+ throw new Error("Claude CLI returned an invalid no-code task payload.");
892
+ }
893
+
894
+ if (!["completed", "failed"].includes(result.result)) {
895
+ throw new Error("Claude CLI returned an unsupported no-code task result.");
896
+ }
897
+
898
+ return {
899
+ result: result.result,
900
+ summary: String(result.summary ?? "").trim(),
901
+ epic_facts: Array.isArray(result.epic_facts)
902
+ ? result.epic_facts.map((item) => String(item).trim()).filter(Boolean)
903
+ : [],
904
+ epic_guardrails: Array.isArray(result.epic_guardrails)
905
+ ? result.epic_guardrails.map((item) => String(item).trim()).filter(Boolean)
906
+ : []
907
+ };
908
+ }
909
+
756
910
  function formatClaudeInvocationError(error, timeoutMs) {
757
911
  const stderr = error instanceof Error && "stderr" in error ? String(error.stderr || "") : "";
758
912
  const output = error instanceof Error && "stdout" in error ? String(error.stdout || "") : "";
@@ -895,6 +1049,69 @@ function terminateChildProcess(child) {
895
1049
  });
896
1050
  }
897
1051
 
1052
+ function buildCodeChangeDecisionSystemPrompt() {
1053
+ return [
1054
+ "You are deciding whether a Jira task requires code changes.",
1055
+ "Read the issue description and memory snapshot carefully.",
1056
+ "Output verdict=needs_code if the task involves modifying, adding, or removing code in any repository.",
1057
+ "Output verdict=no_code if the task can be completed without any code changes (e.g. documentation lookup, research, answering questions, configuration through UI, or purely informational tasks).",
1058
+ "The decision must be deterministic based on the information provided.",
1059
+ "Return only structured output matching the provided schema."
1060
+ ].join(" ");
1061
+ }
1062
+
1063
+ function buildCodeChangeDecisionUserPrompt(input) {
1064
+ return `Decide whether this Jira task requires code changes.
1065
+
1066
+ Issue key: ${input.issue.key}
1067
+ Issue URL: ${input.issue.url}
1068
+ Summary: ${input.issue.summary}
1069
+ Status: ${input.issue.status}
1070
+ Description:
1071
+ ${input.issue.descriptionText || "(none)"}
1072
+
1073
+ Memory snapshot:
1074
+ ${JSON.stringify(input.memory, null, 2)}
1075
+
1076
+ Output verdict=needs_code if code changes are required, or verdict=no_code if the task can be completed without any code changes.`;
1077
+ }
1078
+
1079
+ function buildNoCodeTaskSystemPrompt() {
1080
+ return [
1081
+ "You are completing a Jira task that does not require code changes.",
1082
+ "Read the task description and memory snapshot carefully.",
1083
+ "You may inspect the provided repository using read-only tools if that helps complete the task.",
1084
+ "Do not edit files, do not run mutating commands, and do not make code changes.",
1085
+ "Read the epic memory snapshot before starting so you can apply known repo-specific knowledge.",
1086
+ "When writing repo-specific epic memory, include a short repo label in the text itself using the shortest unambiguous name or abbreviation you can derive from the repo URL or local path, for example `landing:` or `claude-teammate:`.",
1087
+ "Do not add a repo label for epic-wide product, business, or workflow knowledge that is not tied to one codebase.",
1088
+ "Do not restate Jira URLs, repo URLs, repo paths, or other epic facts that are already stored separately from the memory bullets.",
1089
+ "Do not store obvious facts, generic advice, or one-off ticket details in epic memory.",
1090
+ "Return result=completed with a summary of what was accomplished, or result=failed with a summary of why it could not be completed.",
1091
+ "Return epic_facts and epic_guardrails only for reusable non-obvious repo or epic knowledge discovered during this task.",
1092
+ "Return only structured output matching the provided schema."
1093
+ ].join(" ");
1094
+ }
1095
+
1096
+ function buildNoCodeTaskUserPrompt(input) {
1097
+ return `Complete this Jira task.
1098
+
1099
+ Issue key: ${input.issue.key}
1100
+ Issue URL: ${input.issue.url}
1101
+ Summary: ${input.issue.summary}
1102
+ Status: ${input.issue.status}
1103
+ Description:
1104
+ ${input.issue.descriptionText || "(none)"}
1105
+
1106
+ Memory snapshot:
1107
+ ${JSON.stringify(input.memory, null, 2)}
1108
+
1109
+ Repository paths:
1110
+ ${input.repoPaths.join("\n")}
1111
+
1112
+ Complete the task and return result=completed with a summary, or result=failed with an explanation.`;
1113
+ }
1114
+
898
1115
  function buildJiraClarificationSystemPrompt() {
899
1116
  return [
900
1117
  "You are clarifying a Jira ticket for an implementation planning workflow.",
@@ -5,9 +5,11 @@ import process from "node:process";
5
5
  import {
6
6
  cleanupClaudeProcesses,
7
7
  runClaudeClarification,
8
+ runClaudeCodeChangeDecision,
8
9
  runClaudeEpicMemoryCleanup,
9
10
  runClaudeEpicMemorySummarize,
10
11
  runClaudeImplementation,
12
+ runClaudeNoCodeTask,
11
13
  runClaudePrReview,
12
14
  runClaudePullRequestCommentReview,
13
15
  runClaudeGitHubIssueReview
@@ -695,6 +697,83 @@ async function processJiraIssue({ issue, jira, github, botUser, config, projectR
695
697
  issue: detail.key,
696
698
  repos: epicMemory.repos.length
697
699
  });
700
+
701
+ if (!issueMemory.code_change_verdict) {
702
+ let decisionResult;
703
+ try {
704
+ decisionResult = await runClaudeCodeChangeDecision({
705
+ issue: detail,
706
+ memory: { epic: buildEpicMemoryPromptSnapshot(epicMemory) },
707
+ repoPaths: epicMemory.repos.map((repo) => repo.local_path).filter(Boolean),
708
+ permissionMode: config.CLAUDE_PERMISSION_MODE,
709
+ timeoutMs: parseOptionalInt(config.CLAUDE_TIMEOUT_MS)
710
+ });
711
+ } catch (error) {
712
+ issueMemory.last_error = error instanceof Error ? error.message : String(error);
713
+ issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
714
+ await logger.error("Claude code change decision failed", { issue: detail.key, error });
715
+ return buildIssueState(detail, epicMemory, issueMemory, null, issueMemory.last_error);
716
+ }
717
+ issueMemory.code_change_verdict = decisionResult.verdict;
718
+ issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
719
+ await logger.info("Code change decision made", {
720
+ issue: detail.key,
721
+ verdict: decisionResult.verdict
722
+ });
723
+ }
724
+
725
+ if (issueMemory.code_change_verdict === "no_code") {
726
+ if (!issueMemory.no_code_result) {
727
+ let taskResult = null;
728
+ try {
729
+ taskResult = await runClaudeNoCodeTask({
730
+ issue: detail,
731
+ memory: { epic: buildEpicMemoryPromptSnapshot(epicMemory) },
732
+ repoPaths: epicMemory.repos.map((repo) => repo.local_path).filter(Boolean),
733
+ permissionMode: config.CLAUDE_PERMISSION_MODE,
734
+ timeoutMs: parseOptionalInt(config.CLAUDE_TIMEOUT_MS)
735
+ });
736
+ } catch (error) {
737
+ issueMemory.no_code_result = "failed";
738
+ issueMemory.no_code_summary = error instanceof Error ? error.message : String(error);
739
+ issueMemory.workflow_state = "failed";
740
+ issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
741
+ await logger.error("Claude no-code task failed", { issue: detail.key, error });
742
+ }
743
+ if (taskResult) {
744
+ epicMemory = await saveMaintainedEpicMemory({
745
+ filePath: epicMemoryRecord.filePath,
746
+ issue: detail,
747
+ epicMemory,
748
+ projectRoot,
749
+ permissionMode: config.CLAUDE_PERMISSION_MODE,
750
+ timeoutMs: parseOptionalInt(config.CLAUDE_TIMEOUT_MS),
751
+ reason: "Updated epic memory from no-code task execution.",
752
+ candidateUpdates: {
753
+ epic_facts: taskResult.epic_facts || [],
754
+ epic_guardrails: taskResult.epic_guardrails || []
755
+ },
756
+ logger
757
+ });
758
+ issueMemory.no_code_result = taskResult.result;
759
+ issueMemory.no_code_summary = taskResult.summary;
760
+ issueMemory.workflow_state = taskResult.result === "completed" ? "completed" : "failed";
761
+ issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
762
+ await logger.info("No-code task finished", {
763
+ issue: detail.key,
764
+ result: taskResult.result
765
+ });
766
+ }
767
+ }
768
+
769
+ const resultComment = issueMemory.no_code_result === "completed"
770
+ ? `Completed: ${issueMemory.no_code_summary}`
771
+ : `Failed: ${issueMemory.no_code_summary}`;
772
+ await ensureJiraComment(detail, jira, botUser, resultComment);
773
+ issueMemory = await saveIssueMemory(issueMemoryRecord.filePath, detail, issueMemory);
774
+ return buildIssueState(detail, epicMemory, issueMemory, "no_code");
775
+ }
776
+
698
777
  issueMemory = await continueGitHubIssueCreation({
699
778
  detail,
700
779
  jira,
@@ -503,9 +503,15 @@ tr:last-child td { border-bottom:none; }
503
503
  tr:hover td { background:var(--bg); }
504
504
 
505
505
  /* ── Pipeline ── */
506
+ .pipeline-scroll {
507
+ overflow-x: auto;
508
+ }
509
+
506
510
  .pipeline {
507
511
  display:grid;
508
- grid-template-columns:repeat(auto-fill,minmax(200px,1fr));
512
+ grid-auto-flow: column;
513
+ grid-auto-columns: minmax(200px, 1fr);
514
+ min-width: max-content;
509
515
  gap:14px;
510
516
  margin-bottom:4px;
511
517
  }
@@ -1370,8 +1376,10 @@ tr.clickable:hover td { background:var(--sky-bg) !important; }
1370
1376
  </div>
1371
1377
  <button class="btn" onclick="refreshAll()">↻ Refresh</button>
1372
1378
  </div>
1373
- <div class="pipeline" id="pipeline-view">
1374
- <div class="empty"><div class="empty-icon">▸</div><div class="empty-text">Loading…</div></div>
1379
+ <div class="pipeline-scroll">
1380
+ <div class="pipeline" id="pipeline-view">
1381
+ <div class="empty"><div class="empty-icon">▸</div><div class="empty-text">Loading…</div></div>
1382
+ </div>
1375
1383
  </div>
1376
1384
  </div>
1377
1385