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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +28 -3
- package/README.zh-TW.md +20 -3
- package/commands/ttt-create.md +1 -1
- package/commands/ttt-done.md +16 -5
- package/commands/ttt-show.md +17 -0
- package/commands/ttt-work-on.md +6 -2
- package/dist/scripts/claim.js +69 -10
- package/dist/scripts/create.d.ts +8 -0
- package/dist/scripts/create.js +36 -6
- package/dist/scripts/done-job.js +22 -10
- package/dist/scripts/lib/adapters/linear-adapter.js +21 -1
- package/dist/scripts/lib/adapters/trello-adapter.js +3 -0
- package/dist/scripts/lib/adapters/types.d.ts +9 -0
- package/dist/scripts/lib/display.d.ts +9 -2
- package/dist/scripts/lib/display.js +13 -3
- package/dist/scripts/lib/done/args.js +23 -4
- package/dist/scripts/lib/done/linear-handler.d.ts +12 -0
- package/dist/scripts/lib/done/linear-handler.js +30 -7
- package/dist/scripts/lib/done/status-check.d.ts +21 -0
- package/dist/scripts/lib/done/status-check.js +37 -0
- package/dist/scripts/lib/done/types.d.ts +4 -0
- package/dist/scripts/lib/git.d.ts +13 -1
- package/dist/scripts/lib/git.js +33 -19
- package/dist/scripts/lib/remote-task.d.ts +4 -0
- package/dist/scripts/lib/remote-task.js +30 -0
- package/dist/scripts/show.js +94 -39
- package/dist/scripts/test-support/linear-mock.d.ts +1 -0
- package/dist/scripts/test-support/linear-mock.js +87 -0
- package/package.json +1 -1
- package/skills/managing-linear-tasks/SKILL.md +5 -0
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
配置設定。
|
package/commands/ttt-create.md
CHANGED
|
@@ -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
|
package/commands/ttt-done.md
CHANGED
|
@@ -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 |
|
package/commands/ttt-show.md
CHANGED
|
@@ -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.
|
package/commands/ttt-work-on.md
CHANGED
|
@@ -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
|
|
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
|
|
package/dist/scripts/claim.js
CHANGED
|
@@ -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
|
-
/**
|
|
92
|
-
|
|
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
|
|
97
|
-
if (
|
|
98
|
-
tasks.push(
|
|
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
|
|
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
|
|
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
|
-
|
|
221
|
-
|
|
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 ? "🔍" : "👷";
|
package/dist/scripts/create.d.ts
CHANGED
|
@@ -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>;
|
package/dist/scripts/create.js
CHANGED
|
@@ -95,20 +95,50 @@ async function resolveStatusId(config, adapter, teamId, statusName) {
|
|
|
95
95
|
}
|
|
96
96
|
return target.id;
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
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 = {
|
package/dist/scripts/done-job.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
93
|
+
console.error(describeNotInProgress(localTask));
|
|
91
94
|
process.exit(1);
|
|
92
95
|
}
|
|
93
96
|
task = localTask;
|
|
94
97
|
}
|
|
95
|
-
// Get
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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: {
|
|
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
|
}
|