team-toon-tack 2.0.3 → 2.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.
package/README.md CHANGED
@@ -10,7 +10,8 @@ Optimized Linear workflow for Claude Code — saves significant tokens compared
10
10
  - **Smart Task Selection** — Auto-pick highest priority unassigned work with `/work-on next`
11
11
  - **Multi-team Support** — Sync and filter issues across multiple teams
12
12
  - **Flexible Sync Modes** — Choose between remote (immediate Linear sync) or local (offline-first, sync later with `--update`)
13
- - **QA/PM Team Support** — Auto-update parent issues in QA/PM team to "Testing" when completing dev tasks
13
+ - **Completion Modes** — Four modes for task completion: simple, strict review, upstream strict, upstream not strict
14
+ - **QA Team Support** — Auto-update parent issues in QA team to "Testing" when completing dev tasks
14
15
  - **Attachment Download** — Auto-download Linear images and files to local `.ttt/output/` for AI vision analysis
15
16
  - **Blocked Status** — Set tasks as blocked when waiting on external dependencies
16
17
  - **Auto Command Setup** — `ttt init` can install Claude Code commands with custom prefix
@@ -30,10 +31,22 @@ ttt init
30
31
  ```
31
32
 
32
33
  During init, you'll configure:
34
+ - **Dev team**: Your development team (single selection)
35
+ - **Dev testing status**: Testing/review status for your dev team (optional)
36
+ - **QA team(s)**: For cross-team parent issue updates, each with its own testing status (optional)
37
+ - **Completion mode**: How task completion is handled (see below)
33
38
  - **Status source**: `remote` (update Linear immediately) or `local` (work offline, sync with `ttt sync --update`)
34
- - **QA/PM team**: For cross-team parent issue updates (parent must be set in Linear)
35
39
  - **Claude Code commands**: Auto-install with optional prefix (e.g., `/ttt:work-on`)
36
40
 
41
+ ### Completion Modes
42
+
43
+ | Mode | Behavior |
44
+ |------|----------|
45
+ | `simple` | Mark task as Done + parent as Done. Default when no QA team configured. |
46
+ | `strict_review` | Mark task to dev testing + parent to QA testing. |
47
+ | `upstream_strict` | Mark task as Done + parent to Testing. Falls back to dev testing if no parent. Default when QA team configured. |
48
+ | `upstream_not_strict` | Mark task as Done + parent to Testing. No fallback if no parent. |
49
+
37
50
  ### 2. Daily Workflow
38
51
 
39
52
  In Claude Code:
package/README.zh-TW.md CHANGED
@@ -10,7 +10,8 @@
10
10
  - **智慧任務挑選** — `/work-on next` 自動選擇最高優先級的未指派工作
11
11
  - **多團隊支援** — 跨多個團隊同步與過濾 issue
12
12
  - **彈性同步模式** — 選擇 remote(即時同步 Linear)或 local(離線優先,稍後用 `--update` 同步)
13
- - **QA/PM 團隊支援** 完成開發任務時自動將 QA/PM 團隊的 parent issue 更新為「Testing」
13
+ - **完成模式**四種任務完成模式:簡單、嚴格審查、上下游嚴格、上下游非嚴格
14
+ - **QA 團隊支援** — 完成開發任務時自動將 QA 團隊的 parent issue 更新為「Testing」
14
15
  - **附件下載** — 自動下載 Linear 圖片和檔案到本地 `.ttt/output/`,供 AI 視覺分析
15
16
  - **阻塞狀態** — 等待外部依賴時可設定任務為 blocked
16
17
  - **自動安裝指令** — `ttt init` 可自動安裝 Claude Code commands,支援自訂前綴
@@ -30,10 +31,22 @@ ttt init
30
31
  ```
31
32
 
32
33
  初始化時會設定:
34
+ - **開發團隊**:你的開發團隊(單選)
35
+ - **開發團隊測試狀態**:開發團隊的 testing/review 狀態(可選)
36
+ - **QA 團隊**:跨團隊 parent issue 更新,各自設定 testing 狀態(可選)
37
+ - **完成模式**:任務完成時的處理方式(見下方說明)
33
38
  - **狀態來源**:`remote`(即時更新 Linear)或 `local`(離線工作,用 `ttt sync --update` 同步)
34
- - **QA/PM 團隊**:跨團隊 parent issue 更新(需在 Linear 設定 parent)
35
39
  - **Claude Code commands**:自動安裝,可選前綴(如 `/ttt:work-on`)
36
40
 
41
+ ### 完成模式
42
+
43
+ | 模式 | 行為 |
44
+ |------|------|
45
+ | `simple` | 任務標記為 Done,parent 也標記為 Done。未設定 QA 團隊時的預設值。 |
46
+ | `strict_review` | 任務標記到開發團隊的 testing 狀態,parent 標記到 QA 團隊的 testing 狀態。 |
47
+ | `upstream_strict` | 任務標記為 Done,parent 移動到 Testing。若無 parent,fallback 到開發團隊的 testing 狀態。設定 QA 團隊時的預設值。 |
48
+ | `upstream_not_strict` | 任務標記為 Done,parent 移動到 Testing。若無 parent 不做 fallback。 |
49
+
37
50
  ### 2. 每日工作流
38
51
 
39
52
  在 Claude Code 中:
@@ -1,5 +1,6 @@
1
1
  import prompts from "prompts";
2
2
  import { getLinearClient, saveLocalConfig, } from "../utils.js";
3
+ import { getDefaultStatusTransitions } from "../lib/config-builder.js";
3
4
  export async function configureTeams(_config, localConfig) {
4
5
  console.log("👥 Configure Teams\n");
5
6
  const client = getLinearClient();
@@ -9,51 +10,182 @@ export async function configureTeams(_config, localConfig) {
9
10
  console.error("No teams found.");
10
11
  process.exit(1);
11
12
  }
12
- // Multi-select teams
13
- const teamsResponse = await prompts({
14
- type: "multiselect",
15
- name: "teamKeys",
16
- message: "Select teams to sync (space to select):",
13
+ // Build team key mapping
14
+ const teamKeyMap = new Map(); // id -> key
15
+ for (const team of teams) {
16
+ const key = team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
17
+ teamKeyMap.set(team.id, key);
18
+ }
19
+ // Fetch workflow states for all teams
20
+ const teamStatesMap = new Map();
21
+ for (const team of teams) {
22
+ try {
23
+ const statesData = await client.workflowStates({
24
+ filter: { team: { id: { eq: team.id } } },
25
+ });
26
+ teamStatesMap.set(team.id, statesData.nodes);
27
+ }
28
+ catch {
29
+ teamStatesMap.set(team.id, []);
30
+ }
31
+ }
32
+ // 1. Select dev team (single)
33
+ console.log("\n👨‍💻 Dev Team:");
34
+ const devTeamResponse = await prompts({
35
+ type: "select",
36
+ name: "teamId",
37
+ message: "Select your dev team:",
17
38
  choices: teams.map((t) => {
18
- const key = t.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
19
- const currentTeams = localConfig.teams || [localConfig.team];
39
+ const key = teamKeyMap.get(t.id) || "";
20
40
  return {
21
41
  title: t.name,
22
- value: key,
23
- selected: currentTeams.includes(key),
42
+ value: t.id,
43
+ selected: key === localConfig.team,
24
44
  };
25
45
  }),
26
- min: 1,
27
46
  });
28
- const selectedTeamKeys = teamsResponse.teamKeys || [localConfig.team];
29
- // If multiple teams, ask for primary
30
- let primaryTeam = localConfig.team;
31
- if (selectedTeamKeys.length > 1) {
32
- const primaryResponse = await prompts({
33
- type: "select",
34
- name: "primary",
35
- message: "Select primary team:",
36
- choices: selectedTeamKeys.map((key) => ({
37
- title: key,
38
- value: key,
47
+ const devTeamId = devTeamResponse.teamId;
48
+ const devTeamKey = teamKeyMap.get(devTeamId) || localConfig.team;
49
+ const devTeam = teams.find((t) => t.id === devTeamId);
50
+ // 2. Select dev team testing status
51
+ const devStates = teamStatesMap.get(devTeamId) || [];
52
+ const devDefaults = getDefaultStatusTransitions(devStates);
53
+ console.log("\n🔍 Dev Team Testing/Review Status:");
54
+ const devTestingResponse = await prompts({
55
+ type: "select",
56
+ name: "testingStatus",
57
+ message: "Select testing/review status for dev team:",
58
+ choices: [
59
+ { title: "(Skip - no testing status)", value: undefined },
60
+ ...devStates.map((s) => ({
61
+ title: `${s.name} (${s.type})`,
62
+ value: s.name,
39
63
  })),
40
- initial: selectedTeamKeys.indexOf(localConfig.team),
64
+ ],
65
+ initial: localConfig.dev_testing_status
66
+ ? devStates.findIndex((s) => s.name === localConfig.dev_testing_status) +
67
+ 1
68
+ : devDefaults.testing
69
+ ? devStates.findIndex((s) => s.name === devDefaults.testing) + 1
70
+ : 0,
71
+ });
72
+ const devTestingStatus = devTestingResponse.testingStatus;
73
+ // 3. Select QA/PM teams (multiple)
74
+ const otherTeams = teams.filter((t) => t.id !== devTeamId);
75
+ const qaPmTeams = [];
76
+ if (otherTeams.length > 0) {
77
+ console.log("\n🔗 QA/PM Teams:");
78
+ const qaPmResponse = await prompts({
79
+ type: "multiselect",
80
+ name: "teamIds",
81
+ message: "Select QA/PM teams for cross-team parent updates (space to select):",
82
+ choices: otherTeams.map((t) => {
83
+ const key = teamKeyMap.get(t.id) || "";
84
+ const currentQaPm = localConfig.qa_pm_teams || [];
85
+ return {
86
+ title: t.name,
87
+ value: t.id,
88
+ selected: currentQaPm.some((qp) => qp.team === key),
89
+ };
90
+ }),
91
+ hint: "- Press space to select, enter to confirm. Leave empty to skip.",
41
92
  });
42
- primaryTeam = primaryResponse.primary || selectedTeamKeys[0];
93
+ const selectedQaPmIds = qaPmResponse.teamIds || [];
94
+ // 4. For each QA/PM team, select testing status
95
+ for (const teamId of selectedQaPmIds) {
96
+ const team = teams.find((t) => t.id === teamId);
97
+ if (!team)
98
+ continue;
99
+ const teamKey = teamKeyMap.get(teamId) || "";
100
+ const teamStates = teamStatesMap.get(teamId) || [];
101
+ const defaults = getDefaultStatusTransitions(teamStates);
102
+ // Find existing config for this team
103
+ const existingConfig = localConfig.qa_pm_teams?.find((qp) => qp.team === teamKey);
104
+ const stateChoices = teamStates.map((s) => ({
105
+ title: `${s.name} (${s.type})`,
106
+ value: s.name,
107
+ }));
108
+ const statusResponse = await prompts({
109
+ type: "select",
110
+ name: "testingStatus",
111
+ message: `Select testing status for ${team.name}:`,
112
+ choices: [
113
+ { title: "(Skip this team)", value: undefined },
114
+ ...stateChoices,
115
+ ],
116
+ initial: existingConfig?.testing_status
117
+ ? stateChoices.findIndex((c) => c.value === existingConfig.testing_status) + 1
118
+ : defaults.testing
119
+ ? stateChoices.findIndex((c) => c.value === defaults.testing) + 1
120
+ : 0,
121
+ });
122
+ if (statusResponse.testingStatus) {
123
+ qaPmTeams.push({
124
+ team: teamKey,
125
+ testing_status: statusResponse.testingStatus,
126
+ });
127
+ }
128
+ }
43
129
  }
44
- else {
45
- primaryTeam = selectedTeamKeys[0];
46
- }
47
- localConfig.team = primaryTeam;
48
- localConfig.teams =
49
- selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined;
130
+ // 5. Select completion mode
131
+ console.log("\n✅ Completion Mode:");
132
+ const currentMode = localConfig.completion_mode ||
133
+ (qaPmTeams.length > 0 ? "upstream_strict" : "simple");
134
+ const defaultModeIndex = currentMode === "simple"
135
+ ? 0
136
+ : currentMode === "strict_review"
137
+ ? 1
138
+ : currentMode === "upstream_strict"
139
+ ? 2
140
+ : 3;
141
+ const modeResponse = await prompts({
142
+ type: "select",
143
+ name: "mode",
144
+ message: "How should tasks be completed?",
145
+ choices: [
146
+ {
147
+ title: "Simple",
148
+ value: "simple",
149
+ description: "Mark task as done directly",
150
+ },
151
+ {
152
+ title: "Strict Review",
153
+ value: "strict_review",
154
+ description: "Mark task to dev team's testing status",
155
+ },
156
+ {
157
+ title: "Upstream Strict (recommended with QA/PM)",
158
+ value: "upstream_strict",
159
+ description: "Done + parent to testing, fallback to testing if no parent",
160
+ },
161
+ {
162
+ title: "Upstream Not Strict",
163
+ value: "upstream_not_strict",
164
+ description: "Done + parent to testing, no fallback",
165
+ },
166
+ ],
167
+ initial: defaultModeIndex,
168
+ });
169
+ const completionMode = modeResponse.mode || (qaPmTeams.length > 0 ? "upstream_strict" : "simple");
170
+ // Update local config
171
+ localConfig.team = devTeamKey;
172
+ localConfig.dev_testing_status = devTestingStatus;
173
+ localConfig.qa_pm_teams = qaPmTeams.length > 0 ? qaPmTeams : undefined;
174
+ localConfig.completion_mode = completionMode;
175
+ // Remove deprecated fields
176
+ delete localConfig.teams;
177
+ delete localConfig.qa_pm_team;
50
178
  await saveLocalConfig(localConfig);
51
179
  console.log("\n✅ Teams updated:");
52
- if (localConfig.teams) {
53
- console.log(` Selected: ${localConfig.teams.join(", ")}`);
54
- console.log(` Primary: ${localConfig.team}`);
180
+ console.log(` Dev Team: ${devTeam?.name || devTeamKey}`);
181
+ if (devTestingStatus) {
182
+ console.log(` Dev Testing Status: ${devTestingStatus}`);
55
183
  }
56
- else {
57
- console.log(` Team: ${localConfig.team}`);
184
+ console.log(` Completion Mode: ${completionMode}`);
185
+ if (qaPmTeams.length > 0) {
186
+ console.log(" QA/PM Teams:");
187
+ for (const qp of qaPmTeams) {
188
+ console.log(` - ${qp.team}: ${qp.testing_status}`);
189
+ }
58
190
  }
59
191
  }
@@ -3,6 +3,86 @@ import { buildCompletionComment, getLatestCommit } from "./lib/git.js";
3
3
  import { addComment, getStatusTransitions, getWorkflowStates, updateIssueStatus, } from "./lib/linear.js";
4
4
  import { syncSingleIssue } from "./lib/sync.js";
5
5
  import { getLinearClient, loadConfig, loadCycleData, loadLocalConfig, } from "./utils.js";
6
+ // Helper function to update parent issue to a specific status
7
+ async function updateParentStatus(parentIssueId, targetStatus, _qaPmTeams, config) {
8
+ try {
9
+ const client = getLinearClient();
10
+ const searchResult = await client.searchIssues(parentIssueId);
11
+ const parentIssue = searchResult.nodes.find((issue) => issue.identifier === parentIssueId);
12
+ if (!parentIssue) {
13
+ return { success: false };
14
+ }
15
+ const parentTeam = await parentIssue.team;
16
+ if (!parentTeam) {
17
+ return { success: false };
18
+ }
19
+ // Find the matching team configuration
20
+ const teamEntries = Object.entries(config.teams);
21
+ const matchingTeamEntry = teamEntries.find(([_, t]) => t.id === parentTeam.id);
22
+ if (!matchingTeamEntry) {
23
+ return { success: false };
24
+ }
25
+ const [parentTeamKey] = matchingTeamEntry;
26
+ // Get workflow states for the parent's team
27
+ const parentStates = await getWorkflowStates(config, parentTeamKey);
28
+ const targetState = parentStates.find((s) => s.name === targetStatus);
29
+ if (!targetState) {
30
+ return { success: false };
31
+ }
32
+ // Update the parent issue
33
+ await client.updateIssue(parentIssue.id, {
34
+ stateId: targetState.id,
35
+ });
36
+ return { success: true, status: targetStatus };
37
+ }
38
+ catch (error) {
39
+ console.error("Failed to update parent issue:", error);
40
+ return { success: false };
41
+ }
42
+ }
43
+ // Helper function to update parent issue to testing status (uses QA team config)
44
+ async function updateParentToTesting(parentIssueId, qaPmTeams, config) {
45
+ try {
46
+ const client = getLinearClient();
47
+ const searchResult = await client.searchIssues(parentIssueId);
48
+ const parentIssue = searchResult.nodes.find((issue) => issue.identifier === parentIssueId);
49
+ if (!parentIssue) {
50
+ return { success: false };
51
+ }
52
+ const parentTeam = await parentIssue.team;
53
+ if (!parentTeam) {
54
+ return { success: false };
55
+ }
56
+ // Find the matching QA/PM team configuration
57
+ const teamEntries = Object.entries(config.teams);
58
+ const matchingTeamEntry = teamEntries.find(([_, t]) => t.id === parentTeam.id);
59
+ if (!matchingTeamEntry) {
60
+ return { success: false };
61
+ }
62
+ const [parentTeamKey] = matchingTeamEntry;
63
+ // Find the QA/PM team config for this parent's team
64
+ const qaPmConfig = qaPmTeams.find((qp) => qp.team === parentTeamKey);
65
+ if (!qaPmConfig) {
66
+ // Parent's team is not in the configured QA/PM teams
67
+ return { success: false };
68
+ }
69
+ // Get workflow states for the parent's team
70
+ const parentStates = await getWorkflowStates(config, parentTeamKey);
71
+ const testingState = parentStates.find((s) => s.name === qaPmConfig.testing_status);
72
+ if (!testingState) {
73
+ return { success: false };
74
+ }
75
+ // Update the parent issue
76
+ await client.updateIssue(parentIssue.id, {
77
+ stateId: testingState.id,
78
+ });
79
+ return { success: true, testingStatus: qaPmConfig.testing_status };
80
+ }
81
+ catch (error) {
82
+ console.error("Failed to update parent issue:", error);
83
+ return { success: false };
84
+ }
85
+ }
6
86
  function parseArgs(args) {
7
87
  let issueId;
8
88
  let message;
@@ -110,10 +190,84 @@ Examples:
110
190
  process.env.LINEAR_API_KEY &&
111
191
  statusSource === "remote") {
112
192
  const transitions = getStatusTransitions(config);
113
- // Update issue to Done
114
- const success = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
115
- if (success) {
116
- console.log(`Linear: ${task.id} → ${transitions.done}`);
193
+ // Determine completion mode
194
+ const completionMode = localConfig.completion_mode ||
195
+ (localConfig.qa_pm_teams && localConfig.qa_pm_teams.length > 0
196
+ ? "upstream_strict"
197
+ : "simple");
198
+ // Get the testing status to use (from dev team)
199
+ const devTestingStatus = localConfig.dev_testing_status || transitions.testing;
200
+ // Execute based on completion mode
201
+ let parentUpdateSuccess = false;
202
+ let parentTestingStatus;
203
+ switch (completionMode) {
204
+ case "simple": {
205
+ // Mark task as done
206
+ const success = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
207
+ if (success) {
208
+ console.log(`Linear: ${task.id} → ${transitions.done}`);
209
+ }
210
+ // Also mark parent as done if exists
211
+ if (task.parentIssueId) {
212
+ const result = await updateParentStatus(task.parentIssueId, transitions.done, localConfig.qa_pm_teams, config);
213
+ if (result.success) {
214
+ console.log(`Linear: Parent ${task.parentIssueId} → ${transitions.done}`);
215
+ }
216
+ }
217
+ break;
218
+ }
219
+ case "strict_review": {
220
+ // Mark task to dev team's testing status
221
+ if (devTestingStatus) {
222
+ const success = await updateIssueStatus(task.linearId, devTestingStatus, config, localConfig.team);
223
+ if (success) {
224
+ console.log(`Linear: ${task.id} → ${devTestingStatus}`);
225
+ }
226
+ // Also mark parent to testing if exists
227
+ if (task.parentIssueId && localConfig.qa_pm_teams?.length) {
228
+ const result = await updateParentToTesting(task.parentIssueId, localConfig.qa_pm_teams, config);
229
+ if (result.success) {
230
+ console.log(`Linear: Parent ${task.parentIssueId} → ${result.testingStatus}`);
231
+ }
232
+ }
233
+ }
234
+ else {
235
+ console.warn("No dev testing status configured, falling back to done");
236
+ const success = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
237
+ if (success) {
238
+ console.log(`Linear: ${task.id} → ${transitions.done}`);
239
+ }
240
+ }
241
+ break;
242
+ }
243
+ case "upstream_strict":
244
+ case "upstream_not_strict": {
245
+ // First, mark as done
246
+ const doneSuccess = await updateIssueStatus(task.linearId, transitions.done, config, localConfig.team);
247
+ if (doneSuccess) {
248
+ console.log(`Linear: ${task.id} → ${transitions.done}`);
249
+ }
250
+ // Try to update parent to testing
251
+ if (task.parentIssueId && localConfig.qa_pm_teams?.length) {
252
+ const result = await updateParentToTesting(task.parentIssueId, localConfig.qa_pm_teams, config);
253
+ parentUpdateSuccess = result.success;
254
+ parentTestingStatus = result.testingStatus;
255
+ if (parentUpdateSuccess) {
256
+ console.log(`Linear: Parent ${task.parentIssueId} → ${parentTestingStatus}`);
257
+ }
258
+ }
259
+ // Fallback logic for upstream_strict
260
+ if (completionMode === "upstream_strict" &&
261
+ !parentUpdateSuccess &&
262
+ devTestingStatus) {
263
+ // No parent or parent update failed, fallback to testing
264
+ const fallbackSuccess = await updateIssueStatus(task.linearId, devTestingStatus, config, localConfig.team);
265
+ if (fallbackSuccess) {
266
+ console.log(`Linear: ${task.id} → ${devTestingStatus} (fallback, no valid parent)`);
267
+ }
268
+ }
269
+ break;
270
+ }
117
271
  }
118
272
  // Add comment with commit info
119
273
  if (commit) {
@@ -123,43 +277,6 @@ Examples:
123
277
  console.log(`Linear: 已新增 commit 留言`);
124
278
  }
125
279
  }
126
- // Update parent to Testing if exists
127
- if (task.parentIssueId && transitions.testing) {
128
- try {
129
- const client = getLinearClient();
130
- const searchResult = await client.searchIssues(task.parentIssueId);
131
- const parentIssue = searchResult.nodes.find((issue) => issue.identifier === task.parentIssueId);
132
- if (parentIssue) {
133
- const parentTeam = await parentIssue.team;
134
- if (parentTeam) {
135
- // Determine which team key to use for parent's workflow states
136
- // If qa_pm_team is configured and matches parent's team, use it
137
- // Otherwise, try to find the team key from config
138
- let parentTeamKey = localConfig.team;
139
- const teamEntries = Object.entries(config.teams);
140
- const matchingTeam = teamEntries.find(([_, t]) => t.id === parentTeam.id);
141
- if (matchingTeam) {
142
- parentTeamKey = matchingTeam[0];
143
- }
144
- else if (localConfig.qa_pm_team) {
145
- // Fallback to qa_pm_team if configured
146
- parentTeamKey = localConfig.qa_pm_team;
147
- }
148
- const parentStates = await getWorkflowStates(config, parentTeamKey);
149
- const testingState = parentStates.find((s) => s.name === transitions.testing);
150
- if (testingState) {
151
- await client.updateIssue(parentIssue.id, {
152
- stateId: testingState.id,
153
- });
154
- console.log(`Linear: Parent ${task.parentIssueId} → ${transitions.testing}`);
155
- }
156
- }
157
- }
158
- }
159
- catch (parentError) {
160
- console.error("Failed to update parent issue:", parentError);
161
- }
162
- }
163
280
  }
164
281
  else if (statusSource === "local") {
165
282
  console.log(`Local: ${task.id} marked as completed`);
@@ -187,9 +304,6 @@ Examples:
187
304
  if (aiMessage) {
188
305
  console.log(`AI: ${aiMessage}`);
189
306
  }
190
- if (task.parentIssueId && config.status_transitions?.testing) {
191
- console.log(`Parent: ${task.parentIssueId} → ${config.status_transitions.testing}`);
192
- }
193
307
  console.log(`\n🎉 任務完成!`);
194
308
  }
195
309
  doneJob().catch(console.error);
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  import { decode, encode } from "@toon-format/toon";
8
8
  import prompts from "prompts";
9
- import { buildConfig, buildLocalConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
9
+ import { buildConfig, buildLocalConfig, buildTeamsConfig, findTeamKey, findUserKey, getDefaultStatusTransitions, } from "./lib/config-builder.js";
10
10
  import { fileExists, getLinearClient, getPaths, } from "./utils.js";
11
11
  function parseArgs(args) {
12
12
  const options = { interactive: true };
@@ -82,77 +82,157 @@ async function promptForApiKey(options) {
82
82
  }
83
83
  return apiKey;
84
84
  }
85
- async function selectTeams(teams, options) {
86
- let selectedTeams = [teams[0]];
87
- let primaryTeam = teams[0];
85
+ async function selectDevTeam(teams, options) {
86
+ let devTeam = teams[0];
88
87
  if (options.team) {
89
88
  const found = teams.find((t) => t.name.toLowerCase() === options.team?.toLowerCase());
90
89
  if (found) {
91
- selectedTeams = [found];
92
- primaryTeam = found;
90
+ devTeam = found;
93
91
  }
94
92
  }
95
93
  else if (options.interactive && teams.length > 1) {
94
+ console.log("\n👨‍💻 Dev Team Configuration:");
96
95
  const response = await prompts({
97
- type: "multiselect",
98
- name: "teamIds",
99
- message: "Select teams to sync (space to select, enter to confirm):",
96
+ type: "select",
97
+ name: "teamId",
98
+ message: "Select your dev team (for work-on/done commands):",
100
99
  choices: teams.map((t) => ({ title: t.name, value: t.id })),
101
- min: 1,
102
100
  });
103
- if (response.teamIds && response.teamIds.length > 0) {
104
- selectedTeams = teams.filter((t) => response.teamIds.includes(t.id));
105
- if (selectedTeams.length > 1) {
106
- const primaryResponse = await prompts({
107
- type: "select",
108
- name: "primaryTeamId",
109
- message: "Select your primary team (for work-on/done commands):",
110
- choices: selectedTeams.map((t) => ({ title: t.name, value: t.id })),
111
- });
112
- primaryTeam =
113
- selectedTeams.find((t) => t.id === primaryResponse.primaryTeamId) ||
114
- selectedTeams[0];
115
- }
116
- else {
117
- primaryTeam = selectedTeams[0];
118
- }
101
+ if (response.teamId) {
102
+ devTeam = teams.find((t) => t.id === response.teamId) || teams[0];
119
103
  }
120
104
  }
121
- return { selected: selectedTeams, primary: primaryTeam };
105
+ return devTeam;
106
+ }
107
+ async function selectDevTestingStatus(devStates, options) {
108
+ if (!options.interactive || devStates.length === 0) {
109
+ return getDefaultStatusTransitions(devStates).testing;
110
+ }
111
+ console.log("\n🔍 Dev Team Testing/Review Status:");
112
+ const stateChoices = devStates.map((s) => ({
113
+ title: `${s.name} (${s.type})`,
114
+ value: s.name,
115
+ }));
116
+ const response = await prompts({
117
+ type: "select",
118
+ name: "testingStatus",
119
+ message: "Select testing/review status for dev team (used when strict_review mode):",
120
+ choices: [
121
+ { title: "(Skip - no testing status)", value: undefined },
122
+ ...stateChoices,
123
+ ],
124
+ initial: 0,
125
+ });
126
+ return response.testingStatus;
122
127
  }
123
- async function selectQaPmTeam(teams, primaryTeam, options) {
128
+ async function selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options) {
124
129
  // Only ask if there are multiple teams and interactive mode
125
130
  if (!options.interactive || teams.length <= 1) {
126
- return undefined;
131
+ return [];
127
132
  }
128
- // Filter out primary team from choices
129
- const otherTeams = teams.filter((t) => t.id !== primaryTeam.id);
133
+ // Filter out dev team from choices
134
+ const otherTeams = teams.filter((t) => t.id !== devTeam.id);
130
135
  if (otherTeams.length === 0) {
131
- return undefined;
136
+ return [];
132
137
  }
133
- console.log("\n🔗 QA/PM Team Configuration:");
138
+ console.log("\n🔗 QA/PM Teams Configuration:");
134
139
  const response = await prompts({
135
- type: "select",
136
- name: "qaPmTeamId",
137
- message: "Select QA/PM team (for cross-team parent issue updates):",
140
+ type: "multiselect",
141
+ name: "qaPmTeamIds",
142
+ message: "Select QA/PM teams for cross-team parent updates (space to select, enter to confirm):",
138
143
  choices: [
139
- {
140
- title: "(None - skip)",
141
- value: undefined,
142
- description: "No cross-team parent updates",
143
- },
144
144
  ...otherTeams.map((t) => ({
145
145
  title: t.name,
146
146
  value: t.id,
147
147
  description: "Parent issues in this team will be updated to Testing",
148
148
  })),
149
149
  ],
150
- initial: 0,
150
+ hint: "- Press space to select, enter to confirm. Leave empty to skip.",
151
151
  });
152
- if (!response.qaPmTeamId) {
153
- return undefined;
152
+ if (!response.qaPmTeamIds || response.qaPmTeamIds.length === 0) {
153
+ return [];
154
+ }
155
+ // For each selected QA/PM team, select its testing status
156
+ const qaPmTeams = [];
157
+ for (const teamId of response.qaPmTeamIds) {
158
+ const team = teams.find((t) => t.id === teamId);
159
+ if (!team)
160
+ continue;
161
+ const teamStates = teamStatesMap.get(teamId) || [];
162
+ const defaults = getDefaultStatusTransitions(teamStates);
163
+ // Find team key
164
+ const teamKey = Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
165
+ team.name.toLowerCase().replace(/[^a-z0-9]/g, "_");
166
+ if (teamStates.length === 0) {
167
+ // No states available, use default
168
+ if (defaults.testing) {
169
+ qaPmTeams.push({
170
+ team: teamKey,
171
+ testing_status: defaults.testing,
172
+ });
173
+ }
174
+ continue;
175
+ }
176
+ const stateChoices = teamStates.map((s) => ({
177
+ title: `${s.name} (${s.type})`,
178
+ value: s.name,
179
+ }));
180
+ const statusResponse = await prompts({
181
+ type: "select",
182
+ name: "testingStatus",
183
+ message: `Select testing status for ${team.name}:`,
184
+ choices: [
185
+ { title: "(Skip this team)", value: undefined },
186
+ ...stateChoices,
187
+ ],
188
+ initial: defaults.testing
189
+ ? stateChoices.findIndex((c) => c.value === defaults.testing) + 1
190
+ : 0,
191
+ });
192
+ if (statusResponse.testingStatus) {
193
+ qaPmTeams.push({
194
+ team: teamKey,
195
+ testing_status: statusResponse.testingStatus,
196
+ });
197
+ }
154
198
  }
155
- return teams.find((t) => t.id === response.qaPmTeamId);
199
+ return qaPmTeams;
200
+ }
201
+ async function selectCompletionMode(hasQaPmTeams, options) {
202
+ if (!options.interactive) {
203
+ return hasQaPmTeams ? "upstream_strict" : "simple";
204
+ }
205
+ console.log("\n✅ Completion Mode Configuration:");
206
+ const defaultMode = hasQaPmTeams ? 2 : 0; // upstream_strict if has QA/PM teams, else simple
207
+ const response = await prompts({
208
+ type: "select",
209
+ name: "mode",
210
+ message: "How should tasks be completed?",
211
+ choices: [
212
+ {
213
+ title: "Simple",
214
+ value: "simple",
215
+ description: "Mark task as done directly",
216
+ },
217
+ {
218
+ title: "Strict Review",
219
+ value: "strict_review",
220
+ description: "Mark task to dev team's testing status",
221
+ },
222
+ {
223
+ title: "Upstream Strict (recommended with QA/PM)",
224
+ value: "upstream_strict",
225
+ description: "Done + parent to testing, fallback to testing if no parent",
226
+ },
227
+ {
228
+ title: "Upstream Not Strict",
229
+ value: "upstream_not_strict",
230
+ description: "Done + parent to testing, no fallback",
231
+ },
232
+ ],
233
+ initial: defaultMode,
234
+ });
235
+ return response.mode || (hasQaPmTeams ? "upstream_strict" : "simple");
156
236
  }
157
237
  async function selectUser(users, options) {
158
238
  let currentUser = users[0];
@@ -220,19 +300,14 @@ async function selectStatusSource(options) {
220
300
  });
221
301
  return response.statusSource || "remote";
222
302
  }
223
- async function selectStatusMappings(devStates, qaStates, options) {
303
+ async function selectStatusMappings(devStates, options) {
224
304
  // Use dev team states for todo, in_progress, done, blocked
225
- // Use qa team states for testing (fallback to dev team if not set)
305
+ // Testing status is now configured separately (dev_testing_status and qa_pm_teams)
226
306
  const devDefaults = getDefaultStatusTransitions(devStates);
227
- const testingStates = qaStates && qaStates.length > 0 ? qaStates : devStates;
228
- const testingDefaults = getDefaultStatusTransitions(testingStates);
229
307
  if (!options.interactive || devStates.length === 0) {
230
- return {
231
- ...devDefaults,
232
- testing: testingDefaults.testing,
233
- };
308
+ return devDefaults;
234
309
  }
235
- console.log("\n📊 Configure status mappings:");
310
+ console.log("\n📊 Configure status mappings (dev team):");
236
311
  const devStateChoices = devStates.map((s) => ({
237
312
  title: `${s.name} (${s.type})`,
238
313
  value: s.name,
@@ -258,29 +333,8 @@ async function selectStatusMappings(devStates, qaStates, options) {
258
333
  choices: devStateChoices,
259
334
  initial: devStateChoices.findIndex((c) => c.value === devDefaults.done),
260
335
  });
261
- // Testing uses qa team states (or dev team if qa not set)
262
- const testingStateChoices = testingStates.map((s) => ({
263
- title: `${s.name} (${s.type})`,
264
- value: s.name,
265
- }));
266
- const testingChoices = [
267
- { title: "(None)", value: undefined },
268
- ...testingStateChoices,
269
- ];
270
- const testingMessage = qaStates && qaStates.length > 0
271
- ? 'Select status for "Testing" (from QA team, for parent tasks):'
272
- : 'Select status for "Testing" (optional, for parent tasks):';
273
- const testingResponse = await prompts({
274
- type: "select",
275
- name: "testing",
276
- message: testingMessage,
277
- choices: testingChoices,
278
- initial: testingDefaults.testing
279
- ? testingChoices.findIndex((c) => c.value === testingDefaults.testing)
280
- : 0,
281
- });
282
336
  const blockedChoices = [
283
- { title: "(None)", value: undefined },
337
+ { title: "(Skip - no blocked status)", value: undefined },
284
338
  ...devStateChoices,
285
339
  ];
286
340
  const blockedResponse = await prompts({
@@ -296,7 +350,7 @@ async function selectStatusMappings(devStates, qaStates, options) {
296
350
  todo: todoResponse.todo || devDefaults.todo,
297
351
  in_progress: inProgressResponse.in_progress || devDefaults.in_progress,
298
352
  done: doneResponse.done || devDefaults.done,
299
- testing: testingResponse.testing,
353
+ testing: devDefaults.testing, // Will be overridden by dev_testing_status in LocalConfig
300
354
  blocked: blockedResponse.blocked,
301
355
  };
302
356
  }
@@ -536,12 +590,9 @@ async function init() {
536
590
  console.error("Error: No teams found in your Linear workspace.");
537
591
  process.exit(1);
538
592
  }
539
- // Select teams
540
- const { selected: selectedTeams, primary: primaryTeam } = await selectTeams(teams, options);
541
- console.log(` Teams: ${selectedTeams.map((t) => t.name).join(", ")}`);
542
- if (selectedTeams.length > 1) {
543
- console.log(` Primary: ${primaryTeam.name}`);
544
- }
593
+ // Select dev team (single selection)
594
+ const devTeam = await selectDevTeam(teams, options);
595
+ console.log(` Dev Team: ${devTeam.name}`);
545
596
  // Fetch data from ALL teams (not just primary) to support cross-team operations
546
597
  console.log(` Fetching data from ${teams.length} teams...`);
547
598
  // Collect users from all teams, but labels only from primary team
@@ -563,8 +614,8 @@ async function init() {
563
614
  allUsers.push(user);
564
615
  }
565
616
  }
566
- // Labels: only from primary team (dev team)
567
- if (team.id === primaryTeam.id) {
617
+ // Labels: only from dev team
618
+ if (team.id === devTeam.id) {
568
619
  const labelsData = await client.issueLabels({
569
620
  filter: { team: { id: { eq: team.id } } },
570
621
  });
@@ -589,7 +640,7 @@ async function init() {
589
640
  }
590
641
  teamStatesMap.set(team.id, teamStates);
591
642
  }
592
- catch (error) {
643
+ catch {
593
644
  console.warn(` ⚠ Could not fetch data for team ${team.name}, skipping...`);
594
645
  }
595
646
  }
@@ -597,34 +648,34 @@ async function init() {
597
648
  const labels = allLabels;
598
649
  const states = allStates;
599
650
  // Get team-specific states for status mapping
600
- const primaryTeamStates = teamStatesMap.get(primaryTeam.id) || [];
651
+ const devTeamStates = teamStatesMap.get(devTeam.id) || [];
601
652
  console.log(` Users: ${users.length}`);
602
- console.log(` Labels: ${labels.length} (from ${primaryTeam.name})`);
653
+ console.log(` Labels: ${labels.length} (from ${devTeam.name})`);
603
654
  console.log(` Workflow states: ${states.length}`);
604
- // Get cycle from primary team (for current work tracking)
605
- const selectedTeam = await client.team(primaryTeam.id);
655
+ // Get cycle from dev team (for current work tracking)
656
+ const selectedTeam = await client.team(devTeam.id);
606
657
  const currentCycle = (await selectedTeam.activeCycle);
607
658
  // User selections
608
659
  const currentUser = await selectUser(users, options);
609
660
  const defaultLabel = await selectLabelFilter(labels, options);
610
661
  const statusSource = await selectStatusSource(options);
611
- const qaPmTeam = await selectQaPmTeam(teams, primaryTeam, options);
612
- // Get qa team states for testing status mapping (fallback to primary team if not set)
613
- const qaTeamStates = qaPmTeam ? teamStatesMap.get(qaPmTeam.id) : undefined;
614
- const statusTransitions = await selectStatusMappings(primaryTeamStates, qaTeamStates, options);
662
+ // Status transitions for dev team (todo, in_progress, done, blocked)
663
+ const statusTransitions = await selectStatusMappings(devTeamStates, options);
664
+ // Dev team testing status (for strict_review mode)
665
+ const devTestingStatus = await selectDevTestingStatus(devTeamStates, options);
666
+ // Build preliminary teams config for selectQaPmTeams
667
+ const teamsConfig = buildTeamsConfig(teams);
668
+ // QA/PM teams selection (multiple, each with its own testing status)
669
+ const qaPmTeams = await selectQaPmTeams(teams, devTeam, teamStatesMap, teamsConfig, options);
670
+ // Completion mode selection
671
+ const completionMode = await selectCompletionMode(qaPmTeams.length > 0, options);
615
672
  // Build config
616
673
  const config = buildConfig(teams, users, labels, states, statusTransitions, currentCycle ?? undefined);
617
674
  // Find keys
618
675
  const currentUserKey = findUserKey(config.users, currentUser.id);
619
- const primaryTeamKey = findTeamKey(config.teams, primaryTeam.id);
620
- const selectedTeamKeys = selectedTeams
621
- .map((team) => findTeamKey(config.teams, team.id))
622
- .filter((key) => key !== undefined);
623
- const qaPmTeamKey = qaPmTeam
624
- ? findTeamKey(config.teams, qaPmTeam.id)
625
- : undefined;
626
- const localConfig = buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, undefined, // excludeLabels
627
- statusSource, qaPmTeamKey);
676
+ const devTeamKey = findTeamKey(config.teams, devTeam.id);
677
+ const localConfig = buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, undefined, // excludeLabels
678
+ statusSource);
628
679
  // Write config files
629
680
  console.log("\n📝 Writing configuration files...");
630
681
  await fs.mkdir(paths.baseDir, { recursive: true });
@@ -659,10 +710,12 @@ async function init() {
659
710
  localConfig.current_user = existingLocal.current_user;
660
711
  if (existingLocal.team)
661
712
  localConfig.team = existingLocal.team;
662
- if (existingLocal.teams)
663
- localConfig.teams = existingLocal.teams;
664
- if (existingLocal.qa_pm_team)
665
- localConfig.qa_pm_team = existingLocal.qa_pm_team;
713
+ if (existingLocal.dev_testing_status)
714
+ localConfig.dev_testing_status = existingLocal.dev_testing_status;
715
+ if (existingLocal.qa_pm_teams)
716
+ localConfig.qa_pm_teams = existingLocal.qa_pm_teams;
717
+ if (existingLocal.completion_mode)
718
+ localConfig.completion_mode = existingLocal.completion_mode;
666
719
  if (existingLocal.label)
667
720
  localConfig.label = existingLocal.label;
668
721
  if (existingLocal.exclude_labels)
@@ -684,15 +737,19 @@ async function init() {
684
737
  // Summary
685
738
  console.log("\n✅ Initialization complete!\n");
686
739
  console.log("Configuration summary:");
687
- console.log(` Teams: ${selectedTeams.map((t) => t.name).join(", ")}`);
688
- if (selectedTeams.length > 1) {
689
- console.log(` Primary team: ${primaryTeam.name}`);
690
- }
740
+ console.log(` Dev Team: ${devTeam.name}`);
691
741
  console.log(` User: ${currentUser.displayName || currentUser.name} (${currentUser.email})`);
692
742
  console.log(` Label filter: ${defaultLabel || "(none)"}`);
693
743
  console.log(` Status source: ${statusSource === "local" ? "local (use 'sync --update' to push)" : "remote (immediate sync)"}`);
694
- if (qaPmTeam) {
695
- console.log(` QA/PM team: ${qaPmTeam.name}`);
744
+ console.log(` Completion mode: ${completionMode}`);
745
+ if (devTestingStatus) {
746
+ console.log(` Dev testing status: ${devTestingStatus}`);
747
+ }
748
+ if (qaPmTeams.length > 0) {
749
+ console.log(` QA/PM teams:`);
750
+ for (const qaPmTeam of qaPmTeams) {
751
+ console.log(` - ${qaPmTeam.team}: ${qaPmTeam.testing_status}`);
752
+ }
696
753
  }
697
754
  console.log(` (Use 'ttt config filters' to set excluded labels/users)`);
698
755
  if (currentCycle) {
@@ -702,9 +759,6 @@ async function init() {
702
759
  console.log(` Todo: ${statusTransitions.todo}`);
703
760
  console.log(` In Progress: ${statusTransitions.in_progress}`);
704
761
  console.log(` Done: ${statusTransitions.done}`);
705
- if (statusTransitions.testing) {
706
- console.log(` Testing: ${statusTransitions.testing}`);
707
- }
708
762
  if (statusTransitions.blocked) {
709
763
  console.log(` Blocked: ${statusTransitions.blocked}`);
710
764
  }
@@ -1,4 +1,4 @@
1
- import type { Config, LocalConfig, StatusTransitions, TeamConfig, UserConfig, LabelConfig } from "../utils.js";
1
+ import type { CompletionMode, Config, LocalConfig, QaPmTeamConfig, StatusTransitions, TeamConfig, UserConfig, LabelConfig } from "../utils.js";
2
2
  export interface LinearTeam {
3
3
  id: string;
4
4
  name: string;
@@ -38,4 +38,4 @@ export declare function getDefaultStatusTransitions(states: LinearState[]): Stat
38
38
  export declare function buildConfig(teams: LinearTeam[], users: LinearUser[], labels: LinearLabel[], states: LinearState[], statusTransitions: StatusTransitions, currentCycle?: LinearCycle): Config;
39
39
  export declare function findUserKey(usersConfig: Record<string, UserConfig>, userId: string): string;
40
40
  export declare function findTeamKey(teamsConfig: Record<string, TeamConfig>, teamId: string): string;
41
- export declare function buildLocalConfig(currentUserKey: string, primaryTeamKey: string, selectedTeamKeys: string[], defaultLabel?: string, excludeLabels?: string[], statusSource?: "remote" | "local", qaPmTeam?: string): LocalConfig;
41
+ export declare function buildLocalConfig(currentUserKey: string, devTeamKey: string, devTestingStatus?: string, qaPmTeams?: QaPmTeamConfig[], completionMode?: CompletionMode, defaultLabel?: string, excludeLabels?: string[], statusSource?: "remote" | "local"): LocalConfig;
@@ -108,12 +108,13 @@ export function findTeamKey(teamsConfig, teamId) {
108
108
  return (Object.entries(teamsConfig).find(([_, t]) => t.id === teamId)?.[0] ||
109
109
  Object.keys(teamsConfig)[0]);
110
110
  }
111
- export function buildLocalConfig(currentUserKey, primaryTeamKey, selectedTeamKeys, defaultLabel, excludeLabels, statusSource, qaPmTeam) {
111
+ export function buildLocalConfig(currentUserKey, devTeamKey, devTestingStatus, qaPmTeams, completionMode, defaultLabel, excludeLabels, statusSource) {
112
112
  return {
113
113
  current_user: currentUserKey,
114
- team: primaryTeamKey,
115
- teams: selectedTeamKeys.length > 1 ? selectedTeamKeys : undefined,
116
- qa_pm_team: qaPmTeam,
114
+ team: devTeamKey,
115
+ dev_testing_status: devTestingStatus,
116
+ qa_pm_teams: qaPmTeams && qaPmTeams.length > 0 ? qaPmTeams : undefined,
117
+ completion_mode: completionMode,
117
118
  label: defaultLabel,
118
119
  exclude_labels: excludeLabels && excludeLabels.length > 0 ? excludeLabels : undefined,
119
120
  status_source: statusSource,
@@ -24,12 +24,13 @@ export function extractLinearImageUrls(text) {
24
24
  /(https?:\/\/uploads\.linear\.app\/[^\s)>\]]+)/g, // Plain Linear upload URLs
25
25
  ];
26
26
  for (const pattern of patterns) {
27
- let match;
28
- while ((match = pattern.exec(text)) !== null) {
27
+ let match = pattern.exec(text);
28
+ while (match) {
29
29
  const url = match[1];
30
30
  if (isLinearImageUrl(url) && !urls.includes(url)) {
31
31
  urls.push(url);
32
32
  }
33
+ match = pattern.exec(text);
33
34
  }
34
35
  }
35
36
  return urls;
@@ -66,7 +67,7 @@ export async function downloadLinearFile(url, issueId, attachmentId, outputDir)
66
67
  // Linear files require authentication
67
68
  const headers = {};
68
69
  if (process.env.LINEAR_API_KEY) {
69
- headers["Authorization"] = process.env.LINEAR_API_KEY;
70
+ headers.Authorization = process.env.LINEAR_API_KEY;
70
71
  }
71
72
  const response = await fetch(url, { headers });
72
73
  if (!response.ok) {
@@ -35,6 +35,11 @@ export interface StatusTransitions {
35
35
  testing?: string;
36
36
  blocked?: string;
37
37
  }
38
+ export interface QaPmTeamConfig {
39
+ team: string;
40
+ testing_status: string;
41
+ }
42
+ export type CompletionMode = "simple" | "strict_review" | "upstream_strict" | "upstream_not_strict";
38
43
  export interface Config {
39
44
  teams: Record<string, TeamConfig>;
40
45
  users: Record<string, UserConfig>;
@@ -94,11 +99,14 @@ export interface CycleData {
94
99
  export interface LocalConfig {
95
100
  current_user: string;
96
101
  team: string;
97
- teams?: string[];
98
- qa_pm_team?: string;
102
+ dev_testing_status?: string;
103
+ qa_pm_teams?: QaPmTeamConfig[];
104
+ completion_mode?: CompletionMode;
99
105
  exclude_labels?: string[];
100
106
  label?: string;
101
107
  status_source?: "remote" | "local";
108
+ teams?: string[];
109
+ qa_pm_team?: string;
102
110
  }
103
111
  export declare function fileExists(filePath: string): Promise<boolean>;
104
112
  export declare function loadConfig(): Promise<Config>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "2.0.3",
3
+ "version": "2.1.0",
4
4
  "description": "Linear task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {