team-toon-tack 3.10.3 → 3.12.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.
@@ -13,7 +13,7 @@
13
13
  "name": "team-toon-tack",
14
14
  "source": "./",
15
15
  "description": "Linear/Trello task sync & management CLI with commands and skills",
16
- "version": "3.10.3"
16
+ "version": "3.12.0"
17
17
  }
18
18
  ]
19
19
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
3
  "description": "Linear/Trello task sync & management CLI for Claude Code - saves tokens vs MCP",
4
- "version": "3.10.3",
4
+ "version": "3.12.0",
5
5
  "author": {
6
6
  "name": "wayne930242",
7
7
  "email": "wayne930242@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.10.3",
3
+ "version": "3.12.0",
4
4
  "description": "透過 ttt CLI 同步與管理 Linear、Trello 任務。",
5
5
  "author": {
6
6
  "name": "wayne930242"
package/README.md CHANGED
@@ -58,7 +58,7 @@ During init, you'll be prompted to select your task source (Linear or Trello) an
58
58
  |------|----------|
59
59
  | `simple` | Mark task as Done + parent as Done. Default when no QA team configured. |
60
60
  | `strict_review` | Mark task to dev testing + parent to QA testing. |
61
- | `upstream_strict` | Mark task as Done + parent to Testing. Falls back to dev testing if no parent. Default when QA team configured. |
61
+ | `upstream_strict` | Mark task as Done + parent to Testing. Falls back to dev testing if no parent (the task then goes straight to testing, without a Done step). Default when QA team configured. |
62
62
  | `upstream_not_strict` | Mark task as Done + parent to Testing. No fallback if no parent. |
63
63
 
64
64
  > **Note:** Trello always uses simple completion mode as it doesn't support parent issues.
@@ -159,7 +159,14 @@ ttt claim --dry-run # Preview without changes
159
159
  ```
160
160
 
161
161
  Completed, in-review, and blocked tickets are reported and skipped without
162
- stopping the rest of the batch. An unknown ID aborts before anything is claimed.
162
+ stopping the rest of the batch. An ID that local data does not hold (for example
163
+ a Backlog ticket outside the cycle) is fetched from the source, stored locally,
164
+ and claimed like any other; with `--dry-run` it is shown but not stored. An ID
165
+ found neither locally nor at the source aborts before anything is claimed.
166
+ A ticket that is already closed (done or canceled) is reported and skipped. Note
167
+ that a full `ttt sync` rewrites local data from the current cycle, so a stored
168
+ ticket outside it drops out on the next sync (`ttt done <id> --from-remote`
169
+ still completes it).
163
170
 
164
171
  ### `ttt estimate`
165
172
 
@@ -181,9 +188,13 @@ ttt done # Auto-select if only one in-progress
181
188
  ttt done MP-123 # Specific issue
182
189
  ttt done -m "Fixed the bug" # With completion message
183
190
  ttt done MP-123 --from-remote # Fetch from remote (bypasses local data check)
191
+ ttt done MP-123 --commit 1a2b3c4 # Record this commit instead of HEAD
192
+ ttt done MP-123 --commit 1a2b3c4 --repo ~/projects/app # ...read from another repository
184
193
  ```
185
194
 
186
- Use `--from-remote` (or `-r`) when the issue exists in remote but not in local sync data.
195
+ Use `--from-remote` (or `-r`) when the issue exists in remote but not in local sync data, or when the ticket is still Todo and you want to complete it without `ttt claim` (no extra In Progress transition). `ttt done` without it only accepts in-progress tasks.
196
+
197
+ Use `--commit <sha>` (and `--repo <path>` when the commit lives in another checkout) to close a ticket whose commit was merged earlier. Without it, the current HEAD is recorded.
187
198
 
188
199
  ### `ttt status`
189
200
 
@@ -209,8 +220,22 @@ ttt show --label frontend # Filter by label
209
220
  ttt show --status "In Progress" --user me # My in-progress issues
210
221
  ttt show --priority 1 # Show urgent issues
211
222
  ttt show --export # Export as markdown
223
+ ttt show --remote --status Backlog --creator me # Backlog tickets I created
224
+ ttt show --remote --status Backlog --user me # Backlog tickets assigned to me
225
+ ttt show --remote --any-cycle --creator me # My tickets in any cycle
212
226
  ```
213
227
 
228
+ Two options widen a `--remote` search beyond what `ttt sync` stores (both need
229
+ `--remote`; otherwise the command fails instead of ignoring them):
230
+
231
+ - `--creator <me|email|name>` filters by who created the issue (Linear only;
232
+ Trello cards expose no creator, so it fails there). Without `--user`, the
233
+ assignee scope is lifted, so tickets you created for others or left
234
+ unassigned show up.
235
+ - `--any-cycle` searches outside the current cycle. `--status Backlog` implies
236
+ it, because backlog tickets usually belong to no cycle. Trello has no cycles,
237
+ so there it changes nothing.
238
+
214
239
  ### `ttt config`
215
240
 
216
241
  Configure settings.
package/README.zh-TW.md CHANGED
@@ -58,7 +58,7 @@ ttt init
58
58
  |------|------|
59
59
  | `simple` | 任務標記為 Done,parent 也標記為 Done。未設定 QA 團隊時的預設值。 |
60
60
  | `strict_review` | 任務標記到開發團隊的 testing 狀態,parent 標記到 QA 團隊的 testing 狀態。 |
61
- | `upstream_strict` | 任務標記為 Done,parent 移動到 Testing。若無 parent,fallback 到開發團隊的 testing 狀態。設定 QA 團隊時的預設值。 |
61
+ | `upstream_strict` | 任務標記為 Done,parent 移動到 Testing。若無 parent,fallback 到開發團隊的 testing 狀態(此時任務直接進 testing,不會先寫 Done)。設定 QA 團隊時的預設值。 |
62
62
  | `upstream_not_strict` | 任務標記為 Done,parent 移動到 Testing。若無 parent 不做 fallback。 |
63
63
 
64
64
  > **注意**:Trello 因不支援 parent issue,一律使用簡單完成模式。
@@ -159,7 +159,10 @@ ttt claim --dry-run # 預覽,不改狀態
159
159
  ```
160
160
 
161
161
  已完成、審核中、blocked 的票會逐張回報並跳過,不中斷整批。
162
- 批次中出現不存在的 ID 時,整批在動任何票之前中止。
162
+ 本地資料沒有的 ID(例如不在 cycle 內的 Backlog 票)會從來源抓回、存進本地,再照一般流程領票;
163
+ 加 `--dry-run` 時只顯示、不存入。本地與來源都找不到的 ID,整批在動任何票之前中止。
164
+ 已關閉(完成或取消)的票會回報並跳過。
165
+ 注意:完整的 `ttt sync` 會依目前 cycle 重寫本地資料,不在 cycle 內的票下次同步後會消失(仍可用 `ttt done <id> --from-remote` 完成)。
163
166
 
164
167
  ### `ttt estimate`
165
168
 
@@ -181,9 +184,13 @@ ttt done # 若只有一個進行中,自動選擇
181
184
  ttt done MP-123 # 指定 issue
182
185
  ttt done -m "修復了錯誤" # 附上完成說明
183
186
  ttt done MP-123 --from-remote # 從遠端取得(略過本地資料檢查)
187
+ ttt done MP-123 --commit 1a2b3c4 # 記錄指定 commit,而非目前 HEAD
188
+ ttt done MP-123 --commit 1a2b3c4 --repo ~/projects/app # ...從另一個 repository 讀取
184
189
  ```
185
190
 
186
- 當 issue 存在於遠端但未同步到本地資料時,使用 `--from-remote`(或 `-r`)。
191
+ 當 issue 存在於遠端但未同步到本地資料,或 ticket 仍是 Todo、想不經 `ttt claim`(不多一次 In Progress 轉換)直接完成時,使用 `--from-remote`(或 `-r`)。不加此參數時,`ttt done` 只接受進行中的任務。
192
+
193
+ 要關閉 commit 早已合併的 ticket,用 `--commit <sha>`(commit 在另一個 checkout 時再加 `--repo <path>`)。未指定時記錄目前的 HEAD。
187
194
 
188
195
  ### `ttt status`
189
196
 
@@ -209,8 +216,18 @@ ttt show --label frontend # 依標籤過濾
209
216
  ttt show --status "In Progress" --user me # 我進行中的 issue
210
217
  ttt show --priority 1 # 顯示緊急 issue
211
218
  ttt show --export # 輸出為 markdown 格式
219
+ ttt show --remote --status Backlog --creator me # 我建立的 Backlog 票
220
+ ttt show --remote --status Backlog --user me # 指派給我的 Backlog 票
221
+ ttt show --remote --any-cycle --creator me # 我建立、不限 cycle 的票
212
222
  ```
213
223
 
224
+ 兩個選項可把 `--remote` 搜尋範圍放寬到 `ttt sync` 之外(都必須搭配 `--remote`,否則直接報錯,不會默默忽略):
225
+
226
+ - `--creator <me|email|name>`:依建立者過濾(僅 Linear;Trello 卡片沒有建立者,會報錯)。
227
+ 未指定 `--user` 時會解除指派對象的限制,因此你建給別人或未指派的票也找得到。
228
+ - `--any-cycle`:不限目前 cycle。`--status Backlog` 會自動帶上它,因為 Backlog 票通常不屬於任何 cycle。
229
+ Trello 沒有 cycle,所以在 Trello 上沒有作用。
230
+
214
231
  ### `ttt config`
215
232
 
216
233
  配置設定。
@@ -45,7 +45,7 @@ Options:
45
45
  -d, --description <text> Description
46
46
  -a, --assignee <key> Assignee user key from config
47
47
  -p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
48
- -l, --label <names> Label names (comma-separated)
48
+ -l, --label <names> Label names (comma-separated; config first, then looked up live in Linear)
49
49
  -s, --status <name> Initial status name
50
50
  --parent <id> Parent issue identifier (e.g., MP-100)
51
51
  --no-interactive Skip interactive prompts
@@ -11,6 +11,12 @@ arguments:
11
11
  - name: from-remote
12
12
  description: Fetch issue from Linear (bypass local data check)
13
13
  required: false
14
+ - name: commit
15
+ description: Commit to record instead of HEAD (e.g., an already merged commit)
16
+ required: false
17
+ - name: repo
18
+ description: Git repository to read the commit from (default - cwd)
19
+ required: false
14
20
  ---
15
21
 
16
22
  <law>
@@ -24,7 +30,7 @@ DO NOT skip this command — it updates both local and remote status, posts a co
24
30
  ## Execution
25
31
 
26
32
  ```bash
27
- ttt done {{ issue-id }} {{ "-m \"" + message + "\"" if message }} {{ "--from-remote" if from-remote }}
33
+ ttt done {{ issue-id }} {{ "-m \"" + message + "\"" if message }} {{ "--from-remote" if from-remote }} {{ "--commit " + commit if commit }} {{ "--repo " + repo if repo }}
28
34
  ```
29
35
 
30
36
  ### Common Examples
@@ -34,20 +40,24 @@ ttt done # Complete current in-progress task
34
40
  ttt done MP-624 # Complete specific task
35
41
  ttt done -m "Fixed null check" # With completion message
36
42
  ttt done MP-624 -m "Refactored auth" # Specific task with message
37
- ttt done MP-624 --from-remote # Issue not in local data
43
+ ttt done MP-624 --from-remote # Issue not in local data, or still Todo (no claim needed)
44
+ ttt done MP-624 --commit 1a2b3c4 # Record an earlier commit, not HEAD
38
45
  ```
39
46
 
40
47
  ## Full CLI Reference
41
48
 
42
49
  ```
43
- Usage: ttt done [issue-id] [-m message] [--from-remote]
50
+ Usage: ttt done [issue-id] [-m message] [--from-remote] [--commit <sha>] [--repo <path>]
44
51
 
45
52
  Arguments:
46
53
  issue-id Optional. Issue ID if multiple tasks in-progress
47
54
 
48
55
  Options:
49
56
  -m, --message Completion message describing what was done
50
- -r, --from-remote Fetch issue from Linear (bypass local data check)
57
+ -r, --from-remote Fetch issue from Linear (bypass local data check;
58
+ also completes a Todo ticket without `ttt claim`)
59
+ --commit <sha> Record this commit instead of HEAD
60
+ --repo <path> Git repository to read the commit from (default: cwd)
51
61
  ```
52
62
 
53
63
  ## Before Running
@@ -64,7 +74,7 @@ Based on configured completion mode:
64
74
  |------|-------------|---------------|
65
75
  | `simple` | → Done | → Done |
66
76
  | `strict_review` | → Testing | → QA Testing |
67
- | `upstream_strict` | → Done | → Testing |
77
+ | `upstream_strict` | → Done | → Testing (no valid parent: task goes straight to Testing) |
68
78
  | `upstream_not_strict` | → Done | → Testing (no fallback) |
69
79
 
70
80
  Additionally:
@@ -79,4 +89,5 @@ Additionally:
79
89
  | `No in-progress task` | Specify issue-id explicitly |
80
90
  | `Multiple in-progress` | Specify issue-id to disambiguate |
81
91
  | `Issue not found` | Use `--from-remote` flag |
92
+ | `不在進行中狀態` (task is Todo) | `ttt claim <id>` first, or `ttt done <id> --from-remote` |
82
93
  | `No commits found` | Commit your changes first |
@@ -20,6 +20,12 @@ arguments:
20
20
  - name: remote
21
21
  description: Fetch from Linear API instead of local data
22
22
  required: false
23
+ - name: creator
24
+ description: "Filter by creator ('me', email, or name). Needs remote; Linear only"
25
+ required: false
26
+ - name: any-cycle
27
+ description: Search outside the current cycle. Needs remote
28
+ required: false
23
29
  - name: export
24
30
  description: Output as markdown format
25
31
  required: false
@@ -43,6 +49,8 @@ ttt show {{ issue-id }} \
43
49
  {{ "--status \"" + status + "\"" if status }} \
44
50
  {{ "--user " + user if user }} \
45
51
  {{ "--priority " + priority if priority }} \
52
+ {{ "--creator " + creator if creator }} \
53
+ {{ "--any-cycle" if any-cycle }} \
46
54
  {{ "--remote" if remote }} \
47
55
  {{ "--export" if export }}
48
56
  ```
@@ -58,6 +66,9 @@ ttt show --label frontend # Filter by label
58
66
  ttt show --status "In Progress" --user me # My in-progress issues
59
67
  ttt show --priority 1 # All urgent issues
60
68
  ttt show --export # Export all as markdown
69
+ ttt show --remote --status Backlog --creator me # Backlog tickets I created
70
+ ttt show --remote --status Backlog --user me # Backlog tickets assigned to me
71
+ ttt show --remote --any-cycle --creator me # My tickets in any cycle
61
72
  ```
62
73
 
63
74
  ## Full CLI Reference
@@ -75,8 +86,14 @@ Options:
75
86
  --status <status> Filter by status
76
87
  --user <email> Filter by assignee ("me", "unassigned", or email)
77
88
  --priority <n> Filter by priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)
89
+ --creator <who> Filter by creator ("me", email, or name). Needs --remote,
90
+ Linear only. Without --user, the assignee scope is lifted
91
+ --any-cycle Search outside the current cycle. Needs --remote
78
92
  ```
79
93
 
94
+ `--status Backlog` implies `--any-cycle`. `--creator` and `--any-cycle` fail
95
+ without `--remote` instead of being ignored, and `--creator` fails on Trello.
96
+
80
97
  ## After Execution
81
98
 
82
99
  Present the output to the user. If `--export` was used, the output is markdown-formatted and can be used as context.
@@ -52,14 +52,18 @@ Arguments:
52
52
  'next' claims the highest priority pending task
53
53
  'next <n>' claims the top <n> pending tasks
54
54
  If omitted, shows interactive multi-select
55
+ An ID missing from local data (e.g. a Backlog ticket outside
56
+ the cycle) is fetched from the source, stored locally, and
57
+ claimed like any other
55
58
 
56
59
  Options:
57
60
  --dry-run Pick tickets without changing status (preview only)
58
61
  ```
59
62
 
60
63
  Tickets that are already completed, in review, or blocked are reported and
61
- skipped without stopping the rest of the batch. An unknown ID aborts before
62
- anything is claimed.
64
+ skipped without stopping the rest of the batch. An ID found neither locally nor
65
+ at the source aborts before anything is claimed. Find Backlog tickets first with
66
+ `ttt show --remote --status Backlog --creator me` (or `--user me`), then claim by ID.
63
67
 
64
68
  ## After Claiming — Delegate the Implementation
65
69
 
@@ -2,6 +2,8 @@ import { checkbox } from "@inquirer/prompts";
2
2
  import { createAdapter } from "./lib/adapters/index.js";
3
3
  import { displayTaskFull, PRIORITY_LABELS } from "./lib/display.js";
4
4
  import { getStatusTransitions } from "./lib/linear.js";
5
+ import { sourceIssueToTask } from "./lib/remote-task.js";
6
+ import { mapRemoteToLocalStatus } from "./lib/status-helpers.js";
5
7
  import { findTaskByIssueId, getPrioritySortIndex, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
6
8
  const NEXT_ALIASES = ["next", "下一個", "下一個工作"];
7
9
  const NO_PENDING_MESSAGE = "✅ 沒有待處理的任務,所有工作已完成或進行中";
@@ -15,6 +17,9 @@ Arguments:
15
17
  'next' claims the highest priority pending task
16
18
  'next <n>' claims the top <n> pending tasks
17
19
  If omitted, shows interactive multi-select
20
+ An ID missing from local data (e.g. a Backlog ticket outside
21
+ the cycle) is fetched from the source, stored locally, and
22
+ claimed like any other
18
23
 
19
24
  Options:
20
25
  --dry-run Pick tickets without changing status (preview only)
@@ -88,24 +93,65 @@ function selectNext(countArg, pendingTasks) {
88
93
  console.log(`Auto-selected: ${ids.join(", ")}`);
89
94
  return ids;
90
95
  }
91
- /** Resolve the requested tickets, exiting if any ID is unknown. */
92
- function resolveTasks(data, issueIds) {
96
+ /**
97
+ * Fetch a ticket that local data does not hold. Returns null when the source
98
+ * is unreachable or has no such ticket; the caller aborts the batch.
99
+ */
100
+ async function fetchRemoteTask(issueId, config, localConfig) {
101
+ try {
102
+ const adapter = createAdapter(config);
103
+ const issue = await adapter.searchIssue(issueId);
104
+ if (!issue)
105
+ return null;
106
+ const task = sourceIssueToTask(issue, getSourceType(config));
107
+ task.localStatus = mapRemoteToLocalStatus(task.status, getStatusTransitions(config), localConfig);
108
+ // Closed states are named per team ("Canceled", "Duplicate", ...), so
109
+ // the configured done name alone misses them; the state type does not.
110
+ const teamId = config.teams[localConfig.team]?.id;
111
+ if (task.localStatus === "pending" && teamId) {
112
+ const statuses = await adapter.getStatuses(teamId);
113
+ const type = statuses.find((s) => s.name === task.status)?.type;
114
+ if (type === "completed" || type === "canceled") {
115
+ task.localStatus = "completed";
116
+ }
117
+ }
118
+ return task;
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ console.error(`Could not look up ${issueId} remotely: ${message}`);
123
+ return null;
124
+ }
125
+ }
126
+ /**
127
+ * Resolve the requested tickets, exiting if any ID is unknown both locally
128
+ * and at the source. `fetched` holds the tickets that came from the source so
129
+ * the caller can store them once it knows they will be claimed.
130
+ */
131
+ async function resolveTasks(data, issueIds, config, localConfig) {
93
132
  const tasks = [];
133
+ const fetched = new Set();
94
134
  const missing = [];
95
135
  for (const issueId of issueIds) {
96
- const task = findTaskByIssueId(data.tasks, issueId);
97
- if (task) {
98
- tasks.push(task);
136
+ const local = findTaskByIssueId(data.tasks, issueId);
137
+ if (local) {
138
+ tasks.push(local);
139
+ continue;
140
+ }
141
+ const remote = await fetchRemoteTask(issueId, config, localConfig);
142
+ if (remote) {
143
+ tasks.push(remote);
144
+ fetched.add(remote);
99
145
  }
100
146
  else {
101
147
  missing.push(issueId);
102
148
  }
103
149
  }
104
150
  if (missing.length > 0) {
105
- console.error(`Issue ${missing.join(", ")} not found in current cycle. No ticket claimed.`);
151
+ console.error(`Issue ${missing.join(", ")} not found in local data or at the source (use the full ID, e.g. MP-123). No ticket claimed.`);
106
152
  process.exit(1);
107
153
  }
108
- return tasks;
154
+ return { tasks, fetched };
109
155
  }
110
156
  function decide(task) {
111
157
  switch (task.localStatus) {
@@ -211,14 +257,27 @@ async function claim() {
211
257
  const pendingTasks = getPendingTasks(data, config, localConfig);
212
258
  const issueIds = await resolveIssueIds(positional, pendingTasks);
213
259
  // Availability Check — resolve the whole batch before mutating anything
214
- const outcomes = resolveTasks(data, issueIds).map(decide);
260
+ const resolved = await resolveTasks(data, issueIds, config, localConfig);
261
+ const outcomes = resolved.tasks.map(decide);
215
262
  const toClaim = pickTasks(outcomes, (a) => a === "claim");
216
263
  const toDisplay = pickTasks(outcomes, (a) => a !== "skip");
217
264
  if (toDisplay.length === 0) {
218
265
  process.exit(0);
219
266
  }
220
- if (toClaim.length > 0 && !dryRun) {
221
- await applyClaims(toClaim, data, config, localConfig);
267
+ // Fetched tickets are stored when they are claimed or already in progress,
268
+ // so a later `ttt done` finds them; a dry run or a skipped ticket leaves
269
+ // local data untouched.
270
+ if (!dryRun) {
271
+ const toStore = outcomes
272
+ .filter((o) => resolved.fetched.has(o.task) && o.action !== "skip")
273
+ .map((o) => o.task);
274
+ data.tasks.push(...toStore);
275
+ if (toClaim.length > 0) {
276
+ await applyClaims(toClaim, data, config, localConfig);
277
+ }
278
+ else if (toStore.length > 0) {
279
+ await saveCycleData(data);
280
+ }
222
281
  }
223
282
  // Display task info
224
283
  const icon = dryRun ? "🔍" : "👷";
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env bun
2
+ import type { SourceLabel } from "./lib/adapters/types.js";
3
+ import { type Config } from "./utils.js";
4
+ /**
5
+ * Resolve `-l` names to label ids. Config is checked first (key or display
6
+ * name); anything it does not know is looked up live in the source, so a
7
+ * label added after `ttt init` still works.
8
+ */
9
+ export declare function resolveLabelIds(config: Config, labelArg: string | undefined, fetchSourceLabels: () => Promise<SourceLabel[]>): Promise<string[] | undefined>;
2
10
  export declare function create(args?: string[]): Promise<void>;
@@ -95,20 +95,50 @@ async function resolveStatusId(config, adapter, teamId, statusName) {
95
95
  }
96
96
  return target.id;
97
97
  }
98
- function resolveLabelIds(config, labelArg) {
99
- if (!labelArg || !config.labels)
98
+ /**
99
+ * Resolve `-l` names to label ids. Config is checked first (key or display
100
+ * name); anything it does not know is looked up live in the source, so a
101
+ * label added after `ttt init` still works.
102
+ */
103
+ export async function resolveLabelIds(config, labelArg, fetchSourceLabels) {
104
+ if (!labelArg)
100
105
  return undefined;
101
- const names = labelArg.split(",").map((n) => n.trim());
106
+ const names = labelArg
107
+ .split(",")
108
+ .map((n) => n.trim())
109
+ .filter(Boolean);
102
110
  const ids = [];
111
+ const unresolved = [];
103
112
  for (const name of names) {
104
113
  const lower = name.toLowerCase();
105
114
  // Match by config key (e.g. "backend_v3") OR display name (e.g. "Backend-v3")
106
- const entry = Object.entries(config.labels).find(([key, l]) => key.toLowerCase() === lower || l.name.toLowerCase() === lower);
115
+ const entry = Object.entries(config.labels ?? {}).find(([key, l]) => key.toLowerCase() === lower || l.name.toLowerCase() === lower);
107
116
  if (entry) {
108
117
  ids.push(entry[1].id);
109
118
  }
110
119
  else {
111
- console.error(`Warning: label "${name}" not found in config, skipping.`);
120
+ unresolved.push(name);
121
+ }
122
+ }
123
+ if (unresolved.length > 0) {
124
+ let sourceLabels;
125
+ try {
126
+ sourceLabels = await fetchSourceLabels();
127
+ }
128
+ catch {
129
+ // Fall through to the config-only warning below
130
+ }
131
+ for (const name of unresolved) {
132
+ const found = sourceLabels?.find((l) => l.name.toLowerCase() === name.toLowerCase());
133
+ if (found) {
134
+ ids.push(found.id);
135
+ }
136
+ else if (sourceLabels) {
137
+ console.error(`Warning: label "${name}" not found in config or in the source, skipping. Available: ${sourceLabels.map((l) => l.name).join(", ")}`);
138
+ }
139
+ else {
140
+ console.error(`Warning: label "${name}" not found in config (source lookup failed), skipping.`);
141
+ }
112
142
  }
113
143
  }
114
144
  return ids.length > 0 ? ids : undefined;
@@ -216,7 +246,7 @@ Examples:
216
246
  }
217
247
  const assigneeId = resolveAssigneeId(config, assigneeKey);
218
248
  const statusId = await resolveStatusId(config, adapter, teamId, sourceType === "trello" && !statusName ? "todo" : statusName);
219
- const labelIds = resolveLabelIds(config, labelArg);
249
+ const labelIds = await resolveLabelIds(config, labelArg, () => adapter.getLabels(teamId));
220
250
  const parentSourceId = await resolveParentSourceId(adapter, parentId);
221
251
  const currentCycle = await adapter.getCurrentCycle(teamId);
222
252
  const options = {
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { input, select } from "@inquirer/prompts";
7
7
  import { handleLinearCompletion, handleTrelloCompletion, parseArgs, printHelp, } from "./lib/done/index.js";
8
+ import { describeNotInProgress, syncUntilStatus, } from "./lib/done/status-check.js";
8
9
  import { getLatestCommit } from "./lib/git.js";
9
10
  import { fetchIssueDetail, syncSingleIssue } from "./lib/sync.js";
10
11
  import { findTaskByIssueId, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
@@ -14,7 +15,7 @@ async function doneJob() {
14
15
  printHelp();
15
16
  process.exit(0);
16
17
  }
17
- const { issueId: argIssueId, message: argMessage, fromRemote, } = parseArgs(args);
18
+ const { issueId: argIssueId, message: argMessage, fromRemote, commit: commitRef, repo, } = parseArgs(args);
18
19
  let issueId = argIssueId;
19
20
  const config = await loadConfig();
20
21
  const localConfig = await loadLocalConfig();
@@ -45,7 +46,9 @@ async function doneJob() {
45
46
  }
46
47
  // Find in-progress tasks
47
48
  const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
48
- if (inProgressTasks.length === 0) {
49
+ // With an explicit issue ID, fall through so the status guard below can
50
+ // say why that ticket cannot be completed
51
+ if (inProgressTasks.length === 0 && !issueId) {
49
52
  console.log("沒有進行中的任務");
50
53
  process.exit(0);
51
54
  }
@@ -87,13 +90,17 @@ async function doneJob() {
87
90
  process.exit(1);
88
91
  }
89
92
  if (localTask.localStatus !== "in-progress") {
90
- console.log(`⚠️ 任務 ${localTask.id} 不在進行中狀態 (目前: ${localTask.localStatus})`);
93
+ console.error(describeNotInProgress(localTask));
91
94
  process.exit(1);
92
95
  }
93
96
  task = localTask;
94
97
  }
95
- // Get latest commit
96
- const commit = getLatestCommit();
98
+ // Get the commit to record: HEAD by default, or the one named with --commit
99
+ const commit = getLatestCommit({ ref: commitRef, cwd: repo });
100
+ if ((commitRef || repo) && !commit) {
101
+ console.error(`Cannot read commit ${commitRef ?? "HEAD"} from ${repo ?? "the current directory"}. Nothing was changed.`);
102
+ process.exit(1);
103
+ }
97
104
  // Get AI summary message
98
105
  let promptMessage = argMessage || "";
99
106
  if (!promptMessage && process.stdin.isTTY) {
@@ -135,16 +142,18 @@ async function doneJob() {
135
142
  }
136
143
  // Sync full issue data from remote (including new comment)
137
144
  if (sourceType === "linear") {
138
- const syncedTask = await syncSingleIssue(task.id, {
145
+ // Let remote status determine local status; wait out read-after-write lag
146
+ // so a stale read is not recorded as the local state
147
+ const syncedTask = await syncUntilStatus(() => syncSingleIssue(task.id, {
139
148
  config,
140
149
  localConfig,
141
- preserveLocalStatus: false, // Let remote status determine local status
142
- });
150
+ preserveLocalStatus: false,
151
+ }), completionResult?.status);
143
152
  if (syncedTask) {
144
153
  console.log(`Synced: ${syncedTask.id} → ${syncedTask.status} (local: ${syncedTask.localStatus})`);
145
154
  if (completionResult?.status &&
146
155
  syncedTask.status !== completionResult.status) {
147
- console.error(`Remote status verification failed for ${task.id}: expected ${completionResult.status}, got ${syncedTask.status}.`);
156
+ console.error(`Remote status verification failed for ${task.id}: expected ${completionResult.status}, got ${syncedTask.status}. Another actor (e.g. a Git integration or automation) may have changed it; check the issue in Linear.`);
148
157
  process.exit(1);
149
158
  }
150
159
  }
@@ -177,4 +186,7 @@ async function doneJob() {
177
186
  }
178
187
  console.log(`\n🎉 任務完成!`);
179
188
  }
180
- doneJob().catch(console.error);
189
+ doneJob().catch((error) => {
190
+ console.error(error instanceof Error ? error.message : error);
191
+ process.exitCode = 1;
192
+ });
@@ -54,8 +54,13 @@ export class LinearAdapter {
54
54
  }));
55
55
  }
56
56
  async getLabels(teamId) {
57
+ // Workspace-level labels have no team, so a team-only filter misses them
58
+ // even though they can be attached to this team's issues
57
59
  const labelsData = await this.client.issueLabels({
58
- filter: { team: { id: { eq: teamId } } },
60
+ filter: {
61
+ or: [{ team: { id: { eq: teamId } } }, { team: { null: true } }],
62
+ },
63
+ first: 250,
59
64
  });
60
65
  return labelsData.nodes.map((label) => ({
61
66
  id: label.id,
@@ -92,6 +97,21 @@ export class LinearAdapter {
92
97
  if (options.assigneeEmails && options.assigneeEmails.length > 0) {
93
98
  filter.assignee = { email: { in: options.assigneeEmails } };
94
99
  }
100
+ if (options.creator?.emails && options.creator.emails.length > 0) {
101
+ filter.creator = {
102
+ email: { in: options.creator.emails },
103
+ };
104
+ }
105
+ else if (options.creator?.match) {
106
+ const needle = options.creator.match;
107
+ filter.creator = {
108
+ or: [
109
+ { name: { containsIgnoreCase: needle } },
110
+ { displayName: { containsIgnoreCase: needle } },
111
+ { email: { containsIgnoreCase: needle } },
112
+ ],
113
+ };
114
+ }
95
115
  const page = await fetchIssuesPaged(this.client, filter, "fetch issues", options.limit ?? MAX_FETCHED_ISSUES);
96
116
  const issues = [];
97
117
  const excludeLabels = new Set(options.excludeLabels ?? []);
@@ -73,6 +73,9 @@ export class TrelloAdapter {
73
73
  return null;
74
74
  }
75
75
  async getIssues(options) {
76
+ if (options.creator) {
77
+ throw new Error("Trello cards do not expose a creator, so a creator filter is not supported.");
78
+ }
76
79
  const cards = await this.client.getBoardCards(options.teamId);
77
80
  // Ensure lists are cached
78
81
  if (!this.listsCache.has(options.teamId)) {
@@ -87,6 +87,14 @@ export interface SourceIssue {
87
87
  attachments?: SourceAttachment[];
88
88
  comments?: SourceComment[];
89
89
  }
90
+ /**
91
+ * Who created an issue. `emails` matches exactly (OR logic); `match` is a
92
+ * case-insensitive substring over the creator's name, display name and email.
93
+ */
94
+ export interface CreatorFilter {
95
+ emails?: string[];
96
+ match?: string;
97
+ }
90
98
  /**
91
99
  * Options for fetching issues
92
100
  */
@@ -96,6 +104,7 @@ export interface GetIssuesOptions {
96
104
  statusNames?: string[];
97
105
  labelNames?: string[];
98
106
  assigneeEmails?: string[];
107
+ creator?: CreatorFilter;
99
108
  excludeLabels?: string[];
100
109
  limit?: number;
101
110
  }