ai-git-tools 2.1.14 → 2.1.16

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
  \`\`\`
@@ -403,20 +403,41 @@ AI 產出的 notes 會固定整理成以下架構:
403
403
 
404
404
  ### `ai-git-tools redmine-subtasks`
405
405
 
406
- 讀取一個 Redmine Issue,依功能或頁面整理成少量、可獨立交付的大項子任務,並在人工審核後透過 Redmine API 建立 children。以 Issue #18793 為例:
406
+ 這是 Frontend 專用的主 Issue 拆分命令。它會先判斷 Frontend scope,再依頁面或使用者可感知的完整功能整理成少量大項子任務,最後在人工審核後透過 Redmine API 建立 children。以 Issue #18793 為例:
407
407
 
408
408
  ```bash
409
409
  # 只讀取與分析,不會建立子任務
410
- ai-git-tools redmine-subtasks \
411
- --issue 18793 \
412
- --preview \
413
- --output redmine-subtasks.json
410
+ ai-git-tools redmine-subtasks --issue 18793 --preview --output redmine-subtasks.json
414
411
 
415
412
  # 確認草稿中的 title 與 content 後才建立子任務
416
413
  ai-git-tools redmine-subtasks --apply --from redmine-subtasks.json
414
+
415
+ # 重新拆分後,覆蓋該 draft 快照中的既有受控子任務
416
+ ai-git-tools redmine-subtasks --apply --from redmine-subtasks.json --replace
417
+ ```
418
+
419
+ 可以明確指定範圍;目前只接受 `frontend`:
420
+
421
+ ```bash
422
+ ai-git-tools redmine-subtasks --issue 18793 --scope frontend --preview
417
423
  ```
418
424
 
419
- 預設會產生 2~8 個功能/頁面層級的大項;`--max-subtasks <number>` 可設定 1~12 的上限。工具不會把單一 endpoint、component、function 或測試案例拆成獨立 Issue。AI 產生的每個子任務會整理目的、工作範圍、實作重點、行為規則、驗收條件、依賴與待確認事項;`title` 會寫入 Redmine `subject`,完整 `content` 會寫入 `description`。
425
+ 預設會產生 2~8 個功能/頁面層級的大項;`--max-subtasks <number>` 可設定 1~12 的上限。工具不會把單一 endpoint、component、hook、util、function、欄位或測試案例拆成獨立 Issue;明確服務多個頁面的共用 Frontend 基礎設施才可成為前置大項。
426
+
427
+ 如果主 Issue 同時包含 Frontend 與 Backend、Mobile、QA、DevOps 需求,預覽會顯示 Frontend scope decision,只建立 Frontend 子任務,並把排除內容與依據列出。沒有 Frontend 需求時不會保存可套用的空草稿;範圍不明時必須先補充需求。
428
+
429
+ 每個子任務會產生 schema v2 的完整開發內容,包括:
430
+
431
+ - 一句話需求說明、功能需求、包含/不包含範圍、依賴與開發順序。
432
+ - route、存取角色與 evidence;沒有明確 route 時保留未提供,不會猜測路徑。
433
+ - 每個 list、detail、create、edit、view、dashboard、modal 畫面各自列出 screen 與 ASCII wireframe,不會把多個畫面合併成一張模糊示意圖。
434
+ - 使用者流程與 loading、empty、success、error、permission、validation、disabled、unsaved 等有依據的狀態。
435
+ - 每個可操作子任務的 Mermaid 流程圖;無法從需求確認流程時會保留 `not-supported` 狀態與原因,不製造假流程。
436
+ - 有 API 依據時,逐筆保存 method、base URL、path、用途、auth、headers、path/query/body、request example、response status/body、response example、error、pagination、filter、sort 與 completeness。
437
+ - API 只提供 path 或 method 時標記為 partial;沒有 API 依據時維持空的 `apiContract`/`requestResponse`,並列為待確認事項,不使用一般 REST 慣例補欄位。
438
+ - validation、error handling、permission、authentication、authorization、responsive、accessibility、i18n、performance、security、analytics、browser compatibility、cache、polling、feature flag 與 Frontend test plan;未提及的項目使用空值,不會自動套用最佳實務。
439
+
440
+ 內容會區分 `explicit`(Issue 明確要求)、`repository`(安全提供的專案上下文)與 `derived`(為了讓需求可驗收的最小推導)。無法確認的決策會放入 `unresolvedItems`,不會被寫成確定需求。`title` 會寫入 Redmine `subject`,完整 Frontend content 會寫入 `description`。
420
441
 
421
442
  需求能推導出使用流程時,content 會包含 Redmine Mermaid macro:
422
443
 
@@ -430,7 +451,7 @@ flowchart TD
430
451
 
431
452
  子任務涉及頁面但主 Issue 沒有明確畫面設計時,content 會附上純文字 wireframe,只描述資訊區塊、主要操作與已知狀態,不猜測顏色、尺寸或像素。沒有可靠流程或畫面需求時,對應區段會省略,未決事項會保留為待確認項目。
432
453
 
433
- apply 前會重新讀取主 Issue。若 subject、description、project 或 tracker 已變更,預設會停止建立;確認變更仍可套用時才使用 `--force`。既有子任務若有相同的 managed key 或完全相同的 title,會略過以避免重複建立。多筆建立採逐筆處理,部分成功不會回滾已建立的子任務。
454
+ 這個流程使用 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。若重新產生 preview 後要覆蓋上一次拆分的內容,請在 apply 時加上 `--replace`;它只會依 draft 快照順序覆蓋帶有工具 managed marker 的既有子任務,數量增加時建立新的子任務,數量減少時不刪除多出的舊項目。多筆建立或更新採逐筆處理,部分成功不會回滾已建立或已更新的子任務。
434
455
 
435
456
  此命令只建立主 Issue 底下的子任務,不會修改主 Issue 的 status、description、notes、done ratio 或完成欄位。API key 沿用上方的 `REDMINE_API_KEY` 環境變數,不會放入 CLI 參數、草稿或輸出內容;AI 產出的拆分結果仍必須由使用者審核。
436
457
 
@@ -442,7 +463,7 @@ apply 前會重新讀取主 Issue。若 subject、description、project 或 trac
442
463
  export default {
443
464
  // AI 設定
444
465
  ai: {
445
- model: 'claude-haiku-4.5', // AI 模型
466
+ model: 'gpt-5.6-luna', // AI 模型
446
467
  maxDiffLength: 8000, // 最大 diff 長度
447
468
  maxRetries: 3, // 失敗重試次數
448
469
  },
package/bin/cli.js CHANGED
@@ -103,14 +103,21 @@ registerCommand(program, 'redmine-update', '分析並更新 Redmine Issue 的開
103
103
  { flags: '--force', description: '強制略過 Issue 狀態或內容衝突檢查' },
104
104
  ], redmineUpdateCommand);
105
105
 
106
- registerCommand(program, 'redmine-subtasks', '分析主 Issue 並建立功能/頁面子任務', [
107
- { flags: '--issue <id>', description: '指定 Redmine 主 Issue ID' },
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)' },
108
114
  { flags: '--max-subtasks <number>', description: '子任務大項上限(預設:8)' },
109
115
  { flags: '--preview', description: '產生並顯示拆分預覽,不修改 Redmine' },
110
116
  { flags: '--output <file>', description: '保存 preview 草稿 JSON' },
111
117
  { flags: '--apply', description: '套用已審核的子任務草稿' },
112
118
  { flags: '--from <file>', description: '指定要套用的子任務草稿 JSON' },
113
119
  { flags: '--force', description: '強制略過主 Issue 內容衝突檢查' },
120
+ { flags: '--replace', description: '覆蓋 draft 快照中的既有受控子任務' },
114
121
  ], redmineSubtasksCommand);
115
122
 
116
123
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-git-tools",
3
- "version": "2.1.14",
3
+ "version": "2.1.16",
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
  },
@@ -1,4 +1,5 @@
1
- import { readFileSync, writeFileSync } from 'fs';
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { dirname } from 'path';
2
3
  import { loadConfig } from '../core/config-loader.js';
3
4
  import { RedmineClient } from '../redmine/redmine-client.js';
4
5
  import { buildRedmineConfig } from '../redmine/config.js';
@@ -27,11 +28,21 @@ export function validateRedmineSubtaskOptions(options = {}) {
27
28
  if (options.from && !options.apply) throw new Error('--from 只能搭配 --apply 使用');
28
29
  if (options.output && !options.preview) throw new Error('--output 只能搭配 --preview 使用');
29
30
  if (options.force && !options.apply) throw new Error('--force 只能搭配 --apply 使用');
31
+ if (options.replace && !options.apply) throw new Error('--replace 只能搭配 --apply 使用');
32
+ if (options.scope !== undefined && options.scope !== 'frontend') {
33
+ throw new Error('目前 redmine-subtasks 只支援 frontend scope');
34
+ }
30
35
  if (!options.apply && !options.preview) {
31
36
  throw new Error('請使用 --preview 產生草稿,確認後再使用 --apply --from <file>');
32
37
  }
33
38
 
34
39
  const issueIds = normalizeIssueOptions(options.issue);
40
+ if (issueIds.length > 1) {
41
+ throw new Error('redmine-subtasks 只允許指定一個 parent Issue');
42
+ }
43
+ if (options.apply && issueIds.length > 0) {
44
+ throw new Error('--issue 只能搭配 --preview 使用,apply 請以 draft 的 parent 為準');
45
+ }
35
46
  if (!options.apply && issueIds.length === 0) throw new Error('preview 模式需要 --issue <id>');
36
47
  const invalidIssueIds = issueIds.filter(id => !/^[1-9]\d*$/.test(String(id)));
37
48
  if (invalidIssueIds.length > 0) throw new Error(`Issue ID 必須是正整數:${invalidIssueIds.join('、')}`);
@@ -57,10 +68,16 @@ export function formatSubtaskProgress(progress = {}) {
57
68
  return `主 Issue #${progress.parentIssueId} 已讀取:${progress.subject || '無標題'}`;
58
69
  case 'analyze-subtasks':
59
70
  return `分析 Issue #${progress.parentIssueId} 的功能/頁面大項...`;
71
+ case 'scope-analysis':
72
+ return `分析 Issue #${progress.parentIssueId} 的 Frontend scope...`;
60
73
  case 'subtasks-analyzed':
61
74
  return `已產生 ${progress.count} 個子任務候選`;
75
+ case 'screen-enrichment':
76
+ return `[${progress.index}/${progress.total}] 整理 screen wireframe...`;
62
77
  case 'create-child':
63
78
  return `[${progress.index}/${progress.total}] 建立子任務:${progress.title || '無標題'}`;
79
+ case 'replace-child':
80
+ return `[${progress.index}/${progress.total}] 覆蓋子任務:${progress.title || '無標題'}`;
64
81
  case 'preview':
65
82
  return '預覽完成,尚未修改 Redmine';
66
83
  default:
@@ -80,12 +97,17 @@ function printProgress(progress) {
80
97
 
81
98
  function printApplyResults(results) {
82
99
  for (const result of results) {
83
- if (result.created) {
100
+ if (result.updated) {
101
+ console.log(`✅ 子任務「${result.title}」已覆蓋:#${result.childId || '—'}`);
102
+ } else if (result.created) {
84
103
  console.log(`✅ 子任務「${result.title}」已建立:#${result.childId || '—'}`);
85
104
  } else if (result.skipped) {
86
105
  console.log(`⚠️ 子任務「${result.title}」略過:已有相同子任務`);
87
106
  } else if (result.blocked) {
88
- console.log(`❌ Issue #${result.parentIssueId} 已變更,略過全部建立`);
107
+ const reason = result.reason === 'child-detail-unavailable'
108
+ ? `無法讀取既有子任務內容(${result.childIds?.join('、') || '未知'}),為避免重複建立而停止`
109
+ : '已變更,略過全部建立';
110
+ console.log(`❌ 主 Issue #${result.parentIssueId} ${reason}`);
89
111
  } else {
90
112
  console.log(`❌ 子任務「${result.title}」建立失敗:${result.error}`);
91
113
  }
@@ -93,7 +115,7 @@ function printApplyResults(results) {
93
115
  }
94
116
 
95
117
  function hasSubtaskFailures(results = []) {
96
- return results.some(result => result.blocked || result.created === false);
118
+ return results.some(result => result.blocked || result.created === false || result.updated === false);
97
119
  }
98
120
 
99
121
  /**
@@ -114,6 +136,7 @@ export async function redmineSubtasksCommand(options = {}) {
114
136
  draft,
115
137
  client,
116
138
  force: options.force,
139
+ replace: options.replace,
117
140
  onProgress: printProgress,
118
141
  });
119
142
  printApplyResults(results);
@@ -128,14 +151,21 @@ export async function redmineSubtasksCommand(options = {}) {
128
151
  parentIssueId,
129
152
  client,
130
153
  maxSubtasks: options.maxSubtasks === undefined ? 8 : Number(options.maxSubtasks),
154
+ repositoryContext: options.repositoryContext || config.frontend?.repositoryContext || {},
131
155
  model: config.ai.model,
132
156
  maxRetries: config.ai.maxRetries,
133
157
  onProgress: printProgress,
134
158
  });
135
- console.log(formatSubtaskPreview({ parent: draft.parent, candidates: draft.subtasks }));
159
+ console.log(formatSubtaskPreview({
160
+ parent: draft.parent,
161
+ candidates: draft.subtasks,
162
+ analysisScope: draft.analysisScope,
163
+ scopeDecision: draft.scopeDecision,
164
+ }));
136
165
  printProgress({ phase: 'preview' });
137
166
 
138
167
  if (options.output) {
168
+ mkdirSync(dirname(options.output), { recursive: true });
139
169
  writeFileSync(options.output, serializeSubtaskDraft(draft), 'utf-8');
140
170
  console.log(`預覽草稿已保存:${options.output}`);
141
171
  console.log(`請使用 --apply --from ${options.output} 套用。`);
@@ -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
 
@@ -110,13 +110,7 @@ export class AIClient {
110
110
  onPermissionRequest: approveAll,
111
111
  });
112
112
 
113
- // 使用 Promise.race 實現超時控制
114
- const responsePromise = session.sendAndWait({ prompt });
115
- const timeoutPromise = new Promise((_, reject) => {
116
- setTimeout(() => reject(new Error(`AI 請求超時 (${timeout}ms)`)), timeout);
117
- });
118
-
119
- const response = await Promise.race([responsePromise, timeoutPromise]);
113
+ const response = await session.sendAndWait({ prompt }, timeout);
120
114
 
121
115
  const content = response?.data?.content || '';
122
116
  return toTraditionalChinese(content.trim());
@@ -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
 
@@ -160,7 +160,7 @@ export async function analyzeIssue({
160
160
  issue,
161
161
  evidence,
162
162
  aiClient = AIClient,
163
- model = 'claude-haiku-4.5',
163
+ model = 'gpt-5.6-luna',
164
164
  maxRetries = 3,
165
165
  }) {
166
166
  const prompt = buildIssueAnalysisPrompt({ issue, evidence });
@@ -175,14 +175,15 @@ export class RedmineClient {
175
175
 
176
176
  /**
177
177
  * @param {number|string} issueId
178
- * @param {{statusId?: number, doneRatio?: number, dueDate?: string, description?: string, notes?: string, customFields?: Array<object>}} update
178
+ * @param {{statusId?: number, doneRatio?: number, dueDate?: string, subject?: string, description?: string, notes?: string, customFields?: Array<object>}} update
179
179
  * @returns {Promise<object>}
180
180
  */
181
- async updateIssue(issueId, { statusId, doneRatio, dueDate, description, notes, customFields }) {
181
+ async updateIssue(issueId, { statusId, doneRatio, dueDate, subject, description, notes, customFields }) {
182
182
  const issue = {};
183
183
  if (statusId !== undefined && statusId !== null) issue.status_id = statusId;
184
184
  if (doneRatio !== undefined && doneRatio !== null) issue.done_ratio = doneRatio;
185
185
  if (dueDate !== undefined && dueDate !== null) issue.due_date = dueDate;
186
+ if (subject !== undefined) issue.subject = subject;
186
187
  if (description !== undefined) issue.description = description;
187
188
  if (notes) issue.notes = notes;
188
189
  if (Array.isArray(customFields) && customFields.length > 0) {