ai-git-tools 2.1.13 → 2.1.15

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
@@ -147,7 +147,7 @@ npx ai-git-tools commit [選項]
147
147
 
148
148
  \`\`\`bash
149
149
  npx ai-git-tools commit
150
- npx ai-git-tools commit --model claude-haiku-4.5
150
+ npx ai-git-tools commit --model gpt-5.6-luna
151
151
  npx ai-git-tools commit --verbose
152
152
  \`\`\`
153
153
 
@@ -238,7 +238,7 @@ npx ai-git-tools model-info [選項]
238
238
 
239
239
  \`\`\`bash
240
240
  npx ai-git-tools model-info
241
- npx ai-git-tools model-info --model claude-haiku-4.5
241
+ npx ai-git-tools model-info --model gpt-5.6-luna
242
242
  npx ai-git-tools model-info --filter claude
243
243
  npx ai-git-tools model-info --json
244
244
  \`\`\`
@@ -401,6 +401,57 @@ AI 產出的 notes 會固定整理成以下架構:
401
401
 
402
402
  所有 Copilot AI 回應會在程式內統一轉換為台灣繁體中文,包含 Redmine notes、需求說明、實作內容、行為規則、影響範圍與 Mermaid 文字;不依賴模型自行遵守語言設定。
403
403
 
404
+ ### `ai-git-tools redmine-subtasks`
405
+
406
+ 這是 Frontend 專用的主 Issue 拆分命令。它會先判斷 Frontend scope,再依頁面或使用者可感知的完整功能整理成少量大項子任務,最後在人工審核後透過 Redmine API 建立 children。以 Issue #18793 為例:
407
+
408
+ ```bash
409
+ # 只讀取與分析,不會建立子任務
410
+ ai-git-tools redmine-subtasks --issue 18793 --preview --output redmine-subtasks.json
411
+
412
+ # 確認草稿中的 title 與 content 後才建立子任務
413
+ ai-git-tools redmine-subtasks --apply --from redmine-subtasks.json
414
+ ```
415
+
416
+ 可以明確指定範圍;目前只接受 `frontend`:
417
+
418
+ ```bash
419
+ ai-git-tools redmine-subtasks --issue 18793 --scope frontend --preview
420
+ ```
421
+
422
+ 預設會產生 2~8 個功能/頁面層級的大項;`--max-subtasks <number>` 可設定 1~12 的上限。工具不會把單一 endpoint、component、hook、util、function、欄位或測試案例拆成獨立 Issue;明確服務多個頁面的共用 Frontend 基礎設施才可成為前置大項。
423
+
424
+ 如果主 Issue 同時包含 Frontend 與 Backend、Mobile、QA、DevOps 需求,預覽會顯示 Frontend scope decision,只建立 Frontend 子任務,並把排除內容與依據列出。沒有 Frontend 需求時不會保存可套用的空草稿;範圍不明時必須先補充需求。
425
+
426
+ 每個子任務會產生 schema v2 的完整開發內容,包括:
427
+
428
+ - 一句話需求說明、功能需求、包含/不包含範圍、依賴與開發順序。
429
+ - route、存取角色與 evidence;沒有明確 route 時保留未提供,不會猜測路徑。
430
+ - 每個 list、detail、create、edit、view、dashboard、modal 畫面各自列出 screen 與 ASCII wireframe,不會把多個畫面合併成一張模糊示意圖。
431
+ - 使用者流程與 loading、empty、success、error、permission、validation、disabled、unsaved 等有依據的狀態。
432
+ - 每個可操作子任務的 Mermaid 流程圖;無法從需求確認流程時會保留 `not-supported` 狀態與原因,不製造假流程。
433
+ - 有 API 依據時,逐筆保存 method、base URL、path、用途、auth、headers、path/query/body、request example、response status/body、response example、error、pagination、filter、sort 與 completeness。
434
+ - API 只提供 path 或 method 時標記為 partial;沒有 API 依據時維持空的 `apiContract`/`requestResponse`,並列為待確認事項,不使用一般 REST 慣例補欄位。
435
+ - validation、error handling、permission、authentication、authorization、responsive、accessibility、i18n、performance、security、analytics、browser compatibility、cache、polling、feature flag 與 Frontend test plan;未提及的項目使用空值,不會自動套用最佳實務。
436
+
437
+ 內容會區分 `explicit`(Issue 明確要求)、`repository`(安全提供的專案上下文)與 `derived`(為了讓需求可驗收的最小推導)。無法確認的決策會放入 `unresolvedItems`,不會被寫成確定需求。`title` 會寫入 Redmine `subject`,完整 Frontend content 會寫入 `description`。
438
+
439
+ 需求能推導出使用流程時,content 會包含 Redmine Mermaid macro:
440
+
441
+ ```text
442
+ {{mermaid
443
+ flowchart TD
444
+ A[使用者操作] --> B[系統處理]
445
+ B --> C[顯示結果]
446
+ }}
447
+ ```
448
+
449
+ 子任務涉及頁面但主 Issue 沒有明確畫面設計時,content 會附上純文字 wireframe,只描述資訊區塊、主要操作與已知狀態,不猜測顏色、尺寸或像素。沒有可靠流程或畫面需求時,對應區段會省略,未決事項會保留為待確認項目。
450
+
451
+ 這個流程使用 schema v2 draft。舊版 v1 草稿不可直接套用,請重新執行 preview。apply 前會重新讀取主 Issue;若 subject、description、project 或 tracker 已變更,預設會停止建立,確認變更仍可套用時才使用 `--force`。`--force` 只略過 parent snapshot conflict,不會略過 schema、payload 或 duplicate validation。既有子任務若有相同 managed key 或完全相同 title,會略過以避免重複建立;children summary 沒有 description 時,工具會再讀取 child detail 判斷 managed key。多筆建立採逐筆處理,部分成功不會回滾已建立的子任務。
452
+
453
+ 此命令只建立主 Issue 底下的子任務,不會修改主 Issue 的 status、description、notes、done ratio 或完成欄位。API key 沿用上方的 `REDMINE_API_KEY` 環境變數,不會放入 CLI 參數、草稿或輸出內容;AI 產出的拆分結果仍必須由使用者審核。
454
+
404
455
  ## ⚙️ 配置
405
456
 
406
457
  配置檔案範例(\`.ai-git-config.js\`):
@@ -409,7 +460,7 @@ AI 產出的 notes 會固定整理成以下架構:
409
460
  export default {
410
461
  // AI 設定
411
462
  ai: {
412
- model: 'claude-haiku-4.5', // AI 模型
463
+ model: 'gpt-5.6-luna', // AI 模型
413
464
  maxDiffLength: 8000, // 最大 diff 長度
414
465
  maxRetries: 3, // 失敗重試次數
415
466
  },
package/bin/cli.js CHANGED
@@ -17,6 +17,7 @@ import { initCommand } from '../src/commands/init.js';
17
17
  import { usageCommand } from '../src/commands/usage.js';
18
18
  import { modelInfoCommand } from '../src/commands/model-info.js';
19
19
  import { redmineUpdateCommand } from '../src/commands/redmine-update.js';
20
+ import { redmineSubtasksCommand } from '../src/commands/redmine-subtasks.js';
20
21
  import { registerCommand } from '../src/utils/cli-helpers.js';
21
22
 
22
23
  // 讀取 package.json 獲取版本號
@@ -102,4 +103,20 @@ registerCommand(program, 'redmine-update', '分析並更新 Redmine Issue 的開
102
103
  { flags: '--force', description: '強制略過 Issue 狀態或內容衝突檢查' },
103
104
  ], redmineUpdateCommand);
104
105
 
106
+ registerCommand(program, 'redmine-subtasks', '分析主 Issue 的 Frontend 需求並建立功能/頁面子任務', [
107
+ {
108
+ flags: '--issue <id>',
109
+ description: '指定 Redmine 主 Issue ID(僅允許一個)',
110
+ argParser: (value, previous = []) => [...previous, value],
111
+ defaultValue: [],
112
+ },
113
+ { flags: '--scope <scope>', description: '分析範圍(目前只支援 frontend)' },
114
+ { flags: '--max-subtasks <number>', description: '子任務大項上限(預設:8)' },
115
+ { flags: '--preview', description: '產生並顯示拆分預覽,不修改 Redmine' },
116
+ { flags: '--output <file>', description: '保存 preview 草稿 JSON' },
117
+ { flags: '--apply', description: '套用已審核的子任務草稿' },
118
+ { flags: '--from <file>', description: '指定要套用的子任務草稿 JSON' },
119
+ { flags: '--force', description: '強制略過主 Issue 內容衝突檢查' },
120
+ ], redmineSubtasksCommand);
121
+
105
122
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.1.13",
3
+ "version": "2.1.15",
4
4
  "description": "AI-powered Git automation tools for commit messages and PR generation",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -16,7 +16,7 @@ const DEFAULT_CONFIG = `/**
16
16
  export default {
17
17
  // AI 相關配置
18
18
  ai: {
19
- model: 'claude-haiku-4.5', // AI 模型
19
+ model: 'gpt-5.6-luna', // AI 模型
20
20
  maxDiffLength: 8000, // 最大 diff 長度
21
21
  maxRetries: 3, // 最大重試次數
22
22
  },
@@ -0,0 +1,170 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname } from 'path';
3
+ import { loadConfig } from '../core/config-loader.js';
4
+ import { RedmineClient } from '../redmine/redmine-client.js';
5
+ import { buildRedmineConfig } from '../redmine/config.js';
6
+ import {
7
+ applySubtaskDraft,
8
+ generateSubtaskDraft,
9
+ parseSubtaskDraft,
10
+ serializeSubtaskDraft,
11
+ } from '../redmine/subtask-sync.js';
12
+ import { formatSubtaskPreview } from '../redmine/subtask-formatters.js';
13
+ import { Logger } from '../utils/logger.js';
14
+
15
+ const logger = new Logger();
16
+
17
+ function normalizeIssueOptions(issue) {
18
+ return Array.isArray(issue) ? issue : issue === undefined ? [] : [issue];
19
+ }
20
+
21
+ /**
22
+ * 驗證 Redmine 子任務命令參數
23
+ * @param {object} options
24
+ */
25
+ export function validateRedmineSubtaskOptions(options = {}) {
26
+ if (options.apply && options.preview) throw new Error('--apply 與 --preview 不可同時使用');
27
+ if (options.apply && !options.from) throw new Error('--apply 必須搭配 --from <file> 使用');
28
+ if (options.from && !options.apply) throw new Error('--from 只能搭配 --apply 使用');
29
+ if (options.output && !options.preview) throw new Error('--output 只能搭配 --preview 使用');
30
+ if (options.force && !options.apply) throw new Error('--force 只能搭配 --apply 使用');
31
+ if (options.scope !== undefined && options.scope !== 'frontend') {
32
+ throw new Error('目前 redmine-subtasks 只支援 frontend scope');
33
+ }
34
+ if (!options.apply && !options.preview) {
35
+ throw new Error('請使用 --preview 產生草稿,確認後再使用 --apply --from <file>');
36
+ }
37
+
38
+ const issueIds = normalizeIssueOptions(options.issue);
39
+ if (issueIds.length > 1) {
40
+ throw new Error('redmine-subtasks 只允許指定一個 parent Issue');
41
+ }
42
+ if (options.apply && issueIds.length > 0) {
43
+ throw new Error('--issue 只能搭配 --preview 使用,apply 請以 draft 的 parent 為準');
44
+ }
45
+ if (!options.apply && issueIds.length === 0) throw new Error('preview 模式需要 --issue <id>');
46
+ const invalidIssueIds = issueIds.filter(id => !/^[1-9]\d*$/.test(String(id)));
47
+ if (invalidIssueIds.length > 0) throw new Error(`Issue ID 必須是正整數:${invalidIssueIds.join('、')}`);
48
+
49
+ if (options.maxSubtasks !== undefined) {
50
+ const maxSubtasks = Number(options.maxSubtasks);
51
+ if (!Number.isInteger(maxSubtasks) || maxSubtasks < 1 || maxSubtasks > 12) {
52
+ throw new Error('maxSubtasks 必須是 1 到 12 的整數');
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * 格式化子任務處理進度
59
+ * @param {object} progress
60
+ * @returns {string}
61
+ */
62
+ export function formatSubtaskProgress(progress = {}) {
63
+ switch (progress.phase) {
64
+ case 'read-parent':
65
+ return `讀取 Redmine Issue #${progress.parentIssueId}...`;
66
+ case 'parent-read':
67
+ return `主 Issue #${progress.parentIssueId} 已讀取:${progress.subject || '無標題'}`;
68
+ case 'analyze-subtasks':
69
+ return `分析 Issue #${progress.parentIssueId} 的功能/頁面大項...`;
70
+ case 'scope-analysis':
71
+ return `分析 Issue #${progress.parentIssueId} 的 Frontend scope...`;
72
+ case 'subtasks-analyzed':
73
+ return `已產生 ${progress.count} 個子任務候選`;
74
+ case 'screen-enrichment':
75
+ return `[${progress.index}/${progress.total}] 整理 screen wireframe...`;
76
+ case 'create-child':
77
+ return `[${progress.index}/${progress.total}] 建立子任務:${progress.title || '無標題'}`;
78
+ case 'preview':
79
+ return '預覽完成,尚未修改 Redmine';
80
+ default:
81
+ return '';
82
+ }
83
+ }
84
+
85
+ function printProgress(progress) {
86
+ const message = formatSubtaskProgress(progress);
87
+ if (!message) return;
88
+ if (progress.phase === 'parent-read' || progress.phase === 'subtasks-analyzed' || progress.phase === 'preview') {
89
+ logger.success(message);
90
+ } else {
91
+ logger.step(message);
92
+ }
93
+ }
94
+
95
+ function printApplyResults(results) {
96
+ for (const result of results) {
97
+ if (result.created) {
98
+ console.log(`✅ 子任務「${result.title}」已建立:#${result.childId || '—'}`);
99
+ } else if (result.skipped) {
100
+ console.log(`⚠️ 子任務「${result.title}」略過:已有相同子任務`);
101
+ } else if (result.blocked) {
102
+ const reason = result.reason === 'child-detail-unavailable'
103
+ ? `無法讀取既有子任務內容(${result.childIds?.join('、') || '未知'}),為避免重複建立而停止`
104
+ : '已變更,略過全部建立';
105
+ console.log(`❌ 主 Issue #${result.parentIssueId} ${reason}`);
106
+ } else {
107
+ console.log(`❌ 子任務「${result.title}」建立失敗:${result.error}`);
108
+ }
109
+ }
110
+ }
111
+
112
+ function hasSubtaskFailures(results = []) {
113
+ return results.some(result => result.blocked || result.created === false);
114
+ }
115
+
116
+ /**
117
+ * 執行 Redmine 子任務拆分 preview 或 apply
118
+ * @param {object} options
119
+ */
120
+ export async function redmineSubtasksCommand(options = {}) {
121
+ validateRedmineSubtaskOptions(options);
122
+ logger.header('Redmine Issue 子任務拆分');
123
+ const config = await loadConfig();
124
+ const redmineConfig = buildRedmineConfig(config, process.env);
125
+ const client = new RedmineClient(redmineConfig);
126
+
127
+ if (options.apply) {
128
+ logger.step(`讀取已審核草稿:${options.from}`);
129
+ const draft = parseSubtaskDraft(readFileSync(options.from, 'utf-8'));
130
+ const results = await applySubtaskDraft({
131
+ draft,
132
+ client,
133
+ force: options.force,
134
+ onProgress: printProgress,
135
+ });
136
+ printApplyResults(results);
137
+ if (hasSubtaskFailures(results)) {
138
+ throw new Error('部分子任務建立失敗或主 Issue 發生衝突,請處理後重新套用');
139
+ }
140
+ return results;
141
+ }
142
+
143
+ const [parentIssueId] = normalizeIssueOptions(options.issue);
144
+ const draft = await generateSubtaskDraft({
145
+ parentIssueId,
146
+ client,
147
+ maxSubtasks: options.maxSubtasks === undefined ? 8 : Number(options.maxSubtasks),
148
+ repositoryContext: options.repositoryContext || config.frontend?.repositoryContext || {},
149
+ model: config.ai.model,
150
+ maxRetries: config.ai.maxRetries,
151
+ onProgress: printProgress,
152
+ });
153
+ console.log(formatSubtaskPreview({
154
+ parent: draft.parent,
155
+ candidates: draft.subtasks,
156
+ analysisScope: draft.analysisScope,
157
+ scopeDecision: draft.scopeDecision,
158
+ }));
159
+ printProgress({ phase: 'preview' });
160
+
161
+ if (options.output) {
162
+ mkdirSync(dirname(options.output), { recursive: true });
163
+ writeFileSync(options.output, serializeSubtaskDraft(draft), 'utf-8');
164
+ console.log(`預覽草稿已保存:${options.output}`);
165
+ console.log(`請使用 --apply --from ${options.output} 套用。`);
166
+ }
167
+ return draft;
168
+ }
169
+
170
+ export { hasSubtaskFailures };
@@ -98,7 +98,7 @@ export class AIClient {
98
98
  /**
99
99
  * 發送 prompt 並等待回應(帶重試機制和超時保護)
100
100
  */
101
- static async sendAndWait(prompt, model = 'claude-haiku-4.5', maxRetries = 3, timeout = 150000) {
101
+ static async sendAndWait(prompt, model = 'gpt-5.6-luna', maxRetries = 3, timeout = 150000) {
102
102
  AIClient.assertSupportedNodeVersion();
103
103
  let lastError = null;
104
104
 
@@ -160,7 +160,7 @@ export class AIClient {
160
160
  try {
161
161
  return JSON.parse(candidate);
162
162
  } catch {
163
- // 繼續嘗試下一個完整 JSON 區塊
163
+ continue;
164
164
  }
165
165
  }
166
166
 
@@ -12,7 +12,7 @@ import { resolve } from 'path';
12
12
  */
13
13
  const DEFAULT_CONFIG = {
14
14
  ai: {
15
- model: 'claude-haiku-4.5',
15
+ model: 'gpt-5.6-luna',
16
16
  maxDiffLength: 8000,
17
17
  maxRetries: 3,
18
18
  },
@@ -193,7 +193,7 @@ function showHelp() {
193
193
 
194
194
  選項:
195
195
  --base <branch> 指定目標分支 (預設: 使用配置檔的 defaultBase 或自動偵測)
196
- --model <model> 指定 AI 模型 (預設: claude-haiku-4.5)
196
+ --model <model> 指定 AI 模型 (預設: gpt-5.6-luna)
197
197
  --preview 僅預覽 PR 內容,不實際創建
198
198
  --no-confirm 跳過確認直接創建
199
199
  --auto-labels 自動添加 Labels (預設啟用)
@@ -18,7 +18,7 @@ const AI_TIMEOUT_MS = 150000;
18
18
  */
19
19
  export class AIAnalyzer {
20
20
  constructor(config = {}) {
21
- this.model = config.model || 'claude-haiku-4.5';
21
+ this.model = config.model || 'gpt-5.6-luna';
22
22
  this._client = null; // 複用同一個 CopilotClient,避免重複啟動子程序
23
23
  }
24
24
 
@@ -25,7 +25,7 @@ function normalizeFlowcharts(value) {
25
25
  : [];
26
26
 
27
27
  const usedIds = new Set();
28
- return flowcharts.slice(0, 1)
28
+ return flowcharts
29
29
  .filter(flowchart => flowchart && typeof flowchart === 'object')
30
30
  .map((flowchart, index) => {
31
31
  const title = typeof flowchart.title === 'string' ? flowchart.title.trim() : '';
@@ -48,7 +48,8 @@ function normalizeFlowcharts(value) {
48
48
  evidence: asStringArray(flowchart.evidence),
49
49
  };
50
50
  })
51
- .filter(flowchart => flowchart.source);
51
+ .filter(flowchart => flowchart.source)
52
+ .slice(0, 1);
52
53
  }
53
54
 
54
55
  function asStringArray(value) {
@@ -159,7 +160,7 @@ export async function analyzeIssue({
159
160
  issue,
160
161
  evidence,
161
162
  aiClient = AIClient,
162
- model = 'claude-haiku-4.5',
163
+ model = 'gpt-5.6-luna',
163
164
  maxRetries = 3,
164
165
  }) {
165
166
  const prompt = buildIssueAnalysisPrompt({ issue, evidence });
@@ -28,16 +28,30 @@ export class RedmineAPIError extends Error {
28
28
  * @returns {object}
29
29
  */
30
30
  export function normalizeIssue(rawIssue = {}) {
31
+ const project = rawIssue.project || null;
32
+ const tracker = rawIssue.tracker || null;
33
+ const children = rawIssue.children || rawIssue.child_issues || [];
34
+
31
35
  return {
32
36
  id: rawIssue.id,
33
37
  subject: rawIssue.subject || '',
34
38
  description: rawIssue.description || '',
35
39
  status: rawIssue.status || null,
36
- tracker: rawIssue.tracker || null,
40
+ tracker,
41
+ trackerId: rawIssue.tracker_id ?? tracker?.id ?? null,
37
42
  parentId: rawIssue.parent_issue_id || rawIssue.parent?.id || null,
38
43
  customFields: rawIssue.custom_fields || rawIssue.customFields || [],
39
44
  allowedStatuses: rawIssue.allowed_statuses || rawIssue.allowedStatuses || [],
40
- project: rawIssue.project || null,
45
+ project,
46
+ projectId: rawIssue.project_id ?? project?.id ?? null,
47
+ children: Array.isArray(children)
48
+ ? children.map(child => ({
49
+ id: child.id,
50
+ subject: child.subject || '',
51
+ description: child.description || '',
52
+ }))
53
+ : [],
54
+ relations: Array.isArray(rawIssue.relations) ? rawIssue.relations : [],
41
55
  assignedTo: rawIssue.assigned_to || rawIssue.assignedTo || null,
42
56
  raw: rawIssue,
43
57
  };
@@ -121,6 +135,36 @@ export class RedmineClient {
121
135
  return normalizeIssue(data.issue || data);
122
136
  }
123
137
 
138
+ /**
139
+ * @param {{projectId: number|string, trackerId: number|string, subject: string, description: string, parentIssueId: number|string}} input
140
+ * @returns {Promise<object>}
141
+ */
142
+ async createIssue({ projectId, trackerId, subject, description, parentIssueId }) {
143
+ if (projectId === undefined || projectId === null || trackerId === undefined || trackerId === null) {
144
+ throw new Error('建立 Redmine 子任務需要 projectId 與 trackerId');
145
+ }
146
+ if (!subject?.trim() || !description?.trim()) {
147
+ throw new Error('建立 Redmine 子任務需要 subject 與 description');
148
+ }
149
+ if (parentIssueId === undefined || parentIssueId === null) {
150
+ throw new Error('建立 Redmine 子任務需要 parentIssueId');
151
+ }
152
+
153
+ const data = await this.request('/issues.json', {
154
+ method: 'POST',
155
+ json: {
156
+ issue: {
157
+ project_id: projectId,
158
+ tracker_id: trackerId,
159
+ subject,
160
+ description,
161
+ parent_issue_id: parentIssueId,
162
+ },
163
+ },
164
+ });
165
+ return data.issue || data;
166
+ }
167
+
124
168
  /**
125
169
  * @returns {Promise<RedmineStatus[]>}
126
170
  */
@@ -41,7 +41,7 @@ function formatBehaviorRules(rules = []) {
41
41
  function formatFlowcharts(flowcharts = []) {
42
42
  if (!Array.isArray(flowcharts) || flowcharts.length === 0) return '';
43
43
 
44
- return flowcharts.slice(0, 1)
44
+ return flowcharts
45
45
  .map((flowchart, index) => {
46
46
  const mermaid = formatMermaid(flowchart?.source);
47
47
  if (!mermaid) return '';
@@ -50,6 +50,7 @@ function formatFlowcharts(flowcharts = []) {
50
50
  return `### 流程圖:${title}\n\n<!-- flowchart-id: ${id} -->\n${mermaid}\n`;
51
51
  })
52
52
  .filter(Boolean)
53
+ .slice(0, 1)
53
54
  .join('\n');
54
55
  }
55
56