team-toon-tack 3.9.3 → 3.10.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 +16 -9
- package/README.zh-TW.md +16 -9
- package/commands/ttt-comment.md +1 -1
- package/commands/ttt-status.md +1 -1
- package/commands/ttt-work-on.md +34 -19
- package/commands/ttt-write-work-on-skill.md +1 -1
- package/dist/bin/cli.js +9 -6
- package/dist/scripts/claim.js +235 -0
- package/dist/scripts/comment.js +2 -2
- package/dist/scripts/done-job.js +2 -2
- package/dist/scripts/estimate.js +2 -2
- package/dist/scripts/lib/init/file-ops.js +1 -1
- package/dist/scripts/lib/init/linear-init.js +2 -2
- package/dist/scripts/lib/init/prompts.js +1 -1
- package/dist/scripts/lib/init/trello-init.js +2 -2
- package/dist/scripts/status.js +2 -2
- package/dist/scripts/utils.d.ts +14 -0
- package/dist/scripts/utils.js +47 -22
- package/package.json +2 -1
- package/skills/managing-linear-tasks/SKILL.md +7 -6
- package/dist/scripts/work-on.js +0 -153
- /package/dist/scripts/{work-on.d.ts → claim.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Optimized task workflow for Claude Code — supports Linear and Trello, saves si
|
|
|
8
8
|
|
|
9
9
|
- **Token Efficient** — Local cycle cache eliminates repeated API calls, saving significant tokens vs MCP
|
|
10
10
|
- **Multi-source Support** — Works with both Linear and Trello
|
|
11
|
-
- **
|
|
11
|
+
- **Batch Ticket Claiming** — Claim one or many tickets at once with `ttt claim`, or auto-pick your highest priority pending tasks with `ttt claim next <n>`
|
|
12
12
|
- **Multi-team Support** — Sync and filter issues across multiple teams/boards
|
|
13
13
|
- **Flexible Sync Modes** — Choose between remote (immediate sync) or local (offline-first, sync later with `--update`)
|
|
14
14
|
- **Completion Modes** — Four modes for task completion (Linear): simple, strict review, upstream strict, upstream not strict
|
|
@@ -99,7 +99,7 @@ In Claude Code (with plugin installed):
|
|
|
99
99
|
|
|
100
100
|
```
|
|
101
101
|
/ttt:sync # Fetch all issues/cards for current cycle
|
|
102
|
-
/ttt:work-on next #
|
|
102
|
+
/ttt:work-on next # Claim highest priority ticket & start working
|
|
103
103
|
/ttt:estimate MP-123 6 # Save a local 6-hour estimate
|
|
104
104
|
/ttt:done # Complete task with AI-generated summary
|
|
105
105
|
```
|
|
@@ -108,7 +108,7 @@ Or using CLI directly:
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
ttt sync
|
|
111
|
-
ttt
|
|
111
|
+
ttt claim next
|
|
112
112
|
ttt estimate MP-123 6
|
|
113
113
|
ttt done -m "Completed the task"
|
|
114
114
|
```
|
|
@@ -144,16 +144,23 @@ ttt sync MP-123 # Sync specific issue only
|
|
|
144
144
|
ttt sync --update # Push local status changes to remote (for local mode)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
### `ttt
|
|
147
|
+
### `ttt claim`
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
Claim ticket(s): move them to in-progress locally and on the remote source.
|
|
150
|
+
Alias: `ttt work-on`.
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
|
-
ttt
|
|
153
|
-
ttt
|
|
154
|
-
ttt
|
|
153
|
+
ttt claim # Interactive multi-select
|
|
154
|
+
ttt claim MP-123 # Claim one ticket
|
|
155
|
+
ttt claim MP-123 MP-124 # Batch claim
|
|
156
|
+
ttt claim next # Claim highest priority pending task
|
|
157
|
+
ttt claim next 3 # Claim top 3 pending tasks
|
|
158
|
+
ttt claim --dry-run # Preview without changes
|
|
155
159
|
```
|
|
156
160
|
|
|
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.
|
|
163
|
+
|
|
157
164
|
### `ttt estimate`
|
|
158
165
|
|
|
159
166
|
Save a local human-effort estimate that persists in `.ttt/cycle.toon`.
|
|
@@ -268,7 +275,7 @@ Install the plugin for Claude Code integration:
|
|
|
268
275
|
| Command | Description |
|
|
269
276
|
|---------|-------------|
|
|
270
277
|
| `/ttt:sync` | Sync issues to local cycle data |
|
|
271
|
-
| `/ttt:work-on` |
|
|
278
|
+
| `/ttt:work-on` | Claim ticket(s) and work on them |
|
|
272
279
|
| `/ttt:estimate` | Save a local human-effort estimate |
|
|
273
280
|
| `/ttt:done` | Mark current task as completed |
|
|
274
281
|
| `/ttt:status` | Show or modify task status |
|
package/README.zh-TW.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
- **節省 Token** — 本地 cycle 快取避免重複 API 呼叫,比 MCP 省下大量 token
|
|
10
10
|
- **多來源支援** — 支援 Linear 和 Trello
|
|
11
|
-
-
|
|
11
|
+
- **批次領票** — `ttt claim` 可一次領一張或多張票,`ttt claim next <n>` 自動領取最高優先級的待辦任務
|
|
12
12
|
- **多團隊支援** — 跨多個團隊/看板同步與過濾 issue
|
|
13
13
|
- **彈性同步模式** — 選擇 remote(即時同步)或 local(離線優先,稍後用 `--update` 同步)
|
|
14
14
|
- **完成模式** — 四種任務完成模式(Linear):簡單、嚴格審查、上下游嚴格、上下游非嚴格
|
|
@@ -99,7 +99,7 @@ codex plugin add team-toon-tack@ttt-marketplace
|
|
|
99
99
|
|
|
100
100
|
```
|
|
101
101
|
/ttt:sync # 取得當前 cycle 所有 issue/card
|
|
102
|
-
/ttt:work-on next #
|
|
102
|
+
/ttt:work-on next # 領取最高優先級的票並開始工作
|
|
103
103
|
/ttt:estimate MP-123 6 # 寫入本地 6 小時估時
|
|
104
104
|
/ttt:done # 完成任務,附上 AI 生成的摘要
|
|
105
105
|
```
|
|
@@ -108,7 +108,7 @@ codex plugin add team-toon-tack@ttt-marketplace
|
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
110
|
ttt sync
|
|
111
|
-
ttt
|
|
111
|
+
ttt claim next
|
|
112
112
|
ttt estimate MP-123 6
|
|
113
113
|
ttt done -m "完成任務"
|
|
114
114
|
```
|
|
@@ -144,16 +144,23 @@ ttt sync MP-123 # 只同步特定 issue
|
|
|
144
144
|
ttt sync --update # 將本地狀態推送到遠端(local 模式用)
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
### `ttt
|
|
147
|
+
### `ttt claim`
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
領票:把票在本地與遠端來源改成 in-progress。
|
|
150
|
+
別名:`ttt work-on`。
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
|
-
ttt
|
|
153
|
-
ttt
|
|
154
|
-
ttt
|
|
153
|
+
ttt claim # 互動多選
|
|
154
|
+
ttt claim MP-123 # 領一張票
|
|
155
|
+
ttt claim MP-123 MP-124 # 批次領票
|
|
156
|
+
ttt claim next # 領最高優先級的待辦任務
|
|
157
|
+
ttt claim next 3 # 領最高優先級的前 3 張票
|
|
158
|
+
ttt claim --dry-run # 預覽,不改狀態
|
|
155
159
|
```
|
|
156
160
|
|
|
161
|
+
已完成、審核中、blocked 的票會逐張回報並跳過,不中斷整批。
|
|
162
|
+
批次中出現不存在的 ID 時,整批在動任何票之前中止。
|
|
163
|
+
|
|
157
164
|
### `ttt estimate`
|
|
158
165
|
|
|
159
166
|
把人類工程工時估算寫進 `.ttt/cycle.toon`,並在之後的同步中保留下來。
|
|
@@ -268,7 +275,7 @@ your-project/
|
|
|
268
275
|
| 指令 | 說明 |
|
|
269
276
|
|------|------|
|
|
270
277
|
| `/ttt:sync` | 同步 issue 到本地 |
|
|
271
|
-
| `/ttt:work-on` |
|
|
278
|
+
| `/ttt:work-on` | 領票並開始處理 |
|
|
272
279
|
| `/ttt:estimate` | 寫入本地人力估時 |
|
|
273
280
|
| `/ttt:done` | 標記當前任務完成 |
|
|
274
281
|
| `/ttt:status` | 顯示或修改任務狀態 |
|
package/commands/ttt-comment.md
CHANGED
|
@@ -57,5 +57,5 @@ Report:
|
|
|
57
57
|
| Error | Solution |
|
|
58
58
|
|-------|----------|
|
|
59
59
|
| `Message is required` | Add `-m "your message"` |
|
|
60
|
-
| `No in-progress task` | Specify issue-id or run `ttt
|
|
60
|
+
| `No in-progress task` | Specify issue-id or run `ttt claim` first |
|
|
61
61
|
| `Issue not found` | Issue doesn't exist in remote either — check the ID |
|
package/commands/ttt-status.md
CHANGED
|
@@ -80,5 +80,5 @@ If a `--set` operation is performed, the remote task is added to local cycle dat
|
|
|
80
80
|
|
|
81
81
|
| Error | Solution |
|
|
82
82
|
|-------|----------|
|
|
83
|
-
| `No in-progress task` | Specify issue-id or run `ttt
|
|
83
|
+
| `No in-progress task` | Specify issue-id or run `ttt claim` first |
|
|
84
84
|
| `Issue not found` | Issue doesn't exist in remote either — check the ID |
|
package/commands/ttt-work-on.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ttt:work-on
|
|
3
|
-
description:
|
|
3
|
+
description: Claim Linear/Trello ticket(s) and work on them
|
|
4
4
|
arguments:
|
|
5
|
-
- name: issue-
|
|
6
|
-
description: "
|
|
5
|
+
- name: issue-ids
|
|
6
|
+
description: "One or more issue IDs (e.g., MP-624 MP-625), or 'next' / 'next <n>' for auto-select. Defaults to 'next'."
|
|
7
7
|
required: false
|
|
8
8
|
- name: dry-run
|
|
9
9
|
description: Preview selection without changing status
|
|
@@ -11,45 +11,59 @@ arguments:
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
<law>
|
|
14
|
-
YOU MUST execute the `ttt
|
|
14
|
+
YOU MUST execute the `ttt claim` command using the Bash tool.
|
|
15
15
|
DO NOT manually edit cycle.toon or change task status by other means.
|
|
16
|
-
After
|
|
16
|
+
After each task is completed, YOU MUST execute `/ttt:done -m "summary"`. This is MANDATORY.
|
|
17
17
|
</law>
|
|
18
18
|
|
|
19
|
-
# /ttt:work-on —
|
|
19
|
+
# /ttt:work-on — Claim a Ticket and Work on It
|
|
20
|
+
|
|
21
|
+
`ttt claim` is the command; `ttt work-on` is its alias. Claiming moves the ticket
|
|
22
|
+
to in-progress locally and on the remote source.
|
|
20
23
|
|
|
21
24
|
## Execution
|
|
22
25
|
|
|
23
26
|
```bash
|
|
24
|
-
ttt
|
|
27
|
+
ttt claim {{ issue-ids | default: "next" }} {{ "--dry-run" if dry-run }}
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
### Argument Resolution
|
|
28
31
|
|
|
29
32
|
| Input | Command |
|
|
30
33
|
|-------|---------|
|
|
31
|
-
| (none) | `ttt
|
|
32
|
-
| `next` | `ttt
|
|
33
|
-
| `
|
|
34
|
-
|
|
|
35
|
-
| `MP-624
|
|
34
|
+
| (none) | `ttt claim next` |
|
|
35
|
+
| `next` | `ttt claim next` |
|
|
36
|
+
| `next 3` | `ttt claim next 3` |
|
|
37
|
+
| `MP-624` | `ttt claim MP-624` |
|
|
38
|
+
| `MP-624 MP-625` | `ttt claim MP-624 MP-625` |
|
|
39
|
+
| `--dry-run` | `ttt claim next --dry-run` |
|
|
40
|
+
| `MP-624 --dry-run` | `ttt claim MP-624 --dry-run` |
|
|
36
41
|
|
|
37
42
|
## Full CLI Reference
|
|
38
43
|
|
|
39
44
|
```
|
|
40
|
-
Usage: ttt
|
|
45
|
+
Usage: ttt claim [issue-id...] [options]
|
|
46
|
+
|
|
47
|
+
Claim ticket(s): move them to in-progress locally and on the remote source.
|
|
48
|
+
Alias: ttt work-on
|
|
41
49
|
|
|
42
50
|
Arguments:
|
|
43
|
-
issue-id
|
|
44
|
-
|
|
51
|
+
issue-id One or more issue IDs (e.g., MP-624 MP-625)
|
|
52
|
+
'next' claims the highest priority pending task
|
|
53
|
+
'next <n>' claims the top <n> pending tasks
|
|
54
|
+
If omitted, shows interactive multi-select
|
|
45
55
|
|
|
46
56
|
Options:
|
|
47
|
-
--dry-run Pick
|
|
57
|
+
--dry-run Pick tickets without changing status (preview only)
|
|
48
58
|
```
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
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.
|
|
63
|
+
|
|
64
|
+
## After Claiming — Delegate the Implementation
|
|
51
65
|
|
|
52
|
-
This command
|
|
66
|
+
This command claims the tickets. The implementation workflow belongs to the user.
|
|
53
67
|
|
|
54
68
|
**Use the user's own workflow whenever one exists** — routing or laws in their root `CLAUDE.md`, the project `CLAUDE.md`, or a `work-on` / `start-work` skill. Follow it as written and add nothing.
|
|
55
69
|
|
|
@@ -61,7 +75,8 @@ This command picks the task. The implementation workflow belongs to the user.
|
|
|
61
75
|
- Run lint / type / test and report the real output before claiming completion.
|
|
62
76
|
- Offer `/ttt:write-work-on-skill` to capture this project's commands as a reusable skill.
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
When several tickets were claimed at once, carry them one at a time and close
|
|
79
|
+
each with `/ttt:done -m "summary"` before starting the next.
|
|
65
80
|
|
|
66
81
|
## Error Handling
|
|
67
82
|
|
|
@@ -49,7 +49,7 @@ Create `.claude/skills/{{ $1 | default: "work-on" }}/SKILL.md` using the Plan
|
|
|
49
49
|
```markdown
|
|
50
50
|
---
|
|
51
51
|
name: {{ $1 | default: "work-on" }}
|
|
52
|
-
description: Project workflow — Plan → Test → Code → Review. Invoke after ttt
|
|
52
|
+
description: Project workflow — Plan → Test → Code → Review. Invoke after ttt claim claims a ticket.
|
|
53
53
|
---
|
|
54
54
|
|
|
55
55
|
# Work-On Skill ({{ project-name }})
|
package/dist/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const VERSION = pkg.version;
|
|
|
10
10
|
const COMMANDS = [
|
|
11
11
|
"init",
|
|
12
12
|
"sync",
|
|
13
|
+
"claim",
|
|
13
14
|
"work-on",
|
|
14
15
|
"estimate",
|
|
15
16
|
"done",
|
|
@@ -34,7 +35,8 @@ USAGE:
|
|
|
34
35
|
COMMANDS:
|
|
35
36
|
init Initialize config files in current directory
|
|
36
37
|
sync Sync issues from Linear to local cycle.ttt
|
|
37
|
-
|
|
38
|
+
claim Claim ticket(s) and move them to in-progress
|
|
39
|
+
work-on Alias for claim
|
|
38
40
|
estimate Store a local human-effort estimate for a task
|
|
39
41
|
done Mark current task as completed
|
|
40
42
|
status Show or modify task status
|
|
@@ -58,9 +60,9 @@ EXAMPLES:
|
|
|
58
60
|
ttt init # Initialize .ttt directory
|
|
59
61
|
ttt init -d ./custom # Initialize in custom directory
|
|
60
62
|
ttt sync # Sync from Linear
|
|
61
|
-
ttt
|
|
62
|
-
ttt
|
|
63
|
-
ttt
|
|
63
|
+
ttt claim # Interactive multi-select
|
|
64
|
+
ttt claim MP-123 MP-124 # Claim several tickets at once
|
|
65
|
+
ttt claim next 3 # Claim top 3 pending tasks
|
|
64
66
|
ttt estimate MP-123 6 # Save a 6-hour estimate locally
|
|
65
67
|
ttt done # Complete current task
|
|
66
68
|
ttt done -m "Fixed the bug" # With completion message
|
|
@@ -146,9 +148,10 @@ async function main() {
|
|
|
146
148
|
process.argv = ["node", "sync.js", ...commandArgs];
|
|
147
149
|
await importScript("sync.js");
|
|
148
150
|
break;
|
|
151
|
+
case "claim":
|
|
149
152
|
case "work-on":
|
|
150
|
-
process.argv = ["node", "
|
|
151
|
-
await importScript("
|
|
153
|
+
process.argv = ["node", "claim.js", ...commandArgs];
|
|
154
|
+
await importScript("claim.js");
|
|
152
155
|
break;
|
|
153
156
|
case "estimate":
|
|
154
157
|
process.argv = ["node", "estimate.js", ...commandArgs];
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { checkbox } from "@inquirer/prompts";
|
|
2
|
+
import { createAdapter } from "./lib/adapters/index.js";
|
|
3
|
+
import { displayTaskFull, PRIORITY_LABELS } from "./lib/display.js";
|
|
4
|
+
import { getStatusTransitions } from "./lib/linear.js";
|
|
5
|
+
import { findTaskByIssueId, getPrioritySortIndex, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
6
|
+
const NEXT_ALIASES = ["next", "下一個", "下一個工作"];
|
|
7
|
+
const NO_PENDING_MESSAGE = "✅ 沒有待處理的任務,所有工作已完成或進行中";
|
|
8
|
+
const HELP = `Usage: ttt claim [issue-id...] [options]
|
|
9
|
+
|
|
10
|
+
Claim ticket(s): move them to in-progress locally and on the remote source.
|
|
11
|
+
Alias: ttt work-on
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
issue-id One or more issue IDs (e.g., MP-624 MP-625)
|
|
15
|
+
'next' claims the highest priority pending task
|
|
16
|
+
'next <n>' claims the top <n> pending tasks
|
|
17
|
+
If omitted, shows interactive multi-select
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--dry-run Pick tickets without changing status (preview only)
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
ttt claim # Interactive multi-select
|
|
24
|
+
ttt claim MP-624 # Claim one ticket
|
|
25
|
+
ttt claim MP-624 MP-625 # Claim several tickets at once
|
|
26
|
+
ttt claim next # Claim highest priority pending task
|
|
27
|
+
ttt claim next 3 # Claim top 3 pending tasks
|
|
28
|
+
ttt claim --dry-run # Preview without changes`;
|
|
29
|
+
function pickTasks(outcomes, match) {
|
|
30
|
+
return outcomes.filter((o) => match(o.action)).map((o) => o.task);
|
|
31
|
+
}
|
|
32
|
+
/** Pending tasks assigned to the current user, highest priority first. */
|
|
33
|
+
function getPendingTasks(data, config, localConfig) {
|
|
34
|
+
const userKeys = Array.isArray(localConfig.current_user)
|
|
35
|
+
? localConfig.current_user
|
|
36
|
+
: localConfig.current_user
|
|
37
|
+
? [localConfig.current_user]
|
|
38
|
+
: [];
|
|
39
|
+
const currentUserEmails = userKeys
|
|
40
|
+
.map((key) => config.users[key]?.email?.toLowerCase())
|
|
41
|
+
.filter((email) => !!email);
|
|
42
|
+
return data.tasks
|
|
43
|
+
.filter((t) => {
|
|
44
|
+
if (t.localStatus !== "pending")
|
|
45
|
+
return false;
|
|
46
|
+
if (currentUserEmails.length === 0)
|
|
47
|
+
return true; // No filter = all users
|
|
48
|
+
if (!t.assignee)
|
|
49
|
+
return false;
|
|
50
|
+
return currentUserEmails.includes(t.assignee.toLowerCase());
|
|
51
|
+
})
|
|
52
|
+
.sort((a, b) => {
|
|
53
|
+
const pa = getPrioritySortIndex(a.priority, config.priority_order);
|
|
54
|
+
const pb = getPrioritySortIndex(b.priority, config.priority_order);
|
|
55
|
+
return pa - pb;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function selectInteractively(pendingTasks) {
|
|
59
|
+
if (pendingTasks.length === 0) {
|
|
60
|
+
console.log(NO_PENDING_MESSAGE);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
const selected = await checkbox({
|
|
64
|
+
message: "選擇要領的票 (空白鍵多選, Enter 確認):",
|
|
65
|
+
choices: pendingTasks.map((task) => ({
|
|
66
|
+
name: `${PRIORITY_LABELS[task.priority] || "⚪"} ${task.id}: ${task.title}`,
|
|
67
|
+
value: task.id,
|
|
68
|
+
description: task.labels.join(", "),
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
if (selected.length === 0) {
|
|
72
|
+
console.log("已取消");
|
|
73
|
+
process.exit(0);
|
|
74
|
+
}
|
|
75
|
+
return selected;
|
|
76
|
+
}
|
|
77
|
+
function selectNext(countArg, pendingTasks) {
|
|
78
|
+
const count = countArg ? Number.parseInt(countArg, 10) : 1;
|
|
79
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
80
|
+
console.error(`Invalid count: ${countArg}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
if (pendingTasks.length === 0) {
|
|
84
|
+
console.log(NO_PENDING_MESSAGE);
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
const ids = pendingTasks.slice(0, count).map((t) => t.id);
|
|
88
|
+
console.log(`Auto-selected: ${ids.join(", ")}`);
|
|
89
|
+
return ids;
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the requested tickets, exiting if any ID is unknown. */
|
|
92
|
+
function resolveTasks(data, issueIds) {
|
|
93
|
+
const tasks = [];
|
|
94
|
+
const missing = [];
|
|
95
|
+
for (const issueId of issueIds) {
|
|
96
|
+
const task = findTaskByIssueId(data.tasks, issueId);
|
|
97
|
+
if (task) {
|
|
98
|
+
tasks.push(task);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
missing.push(issueId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (missing.length > 0) {
|
|
105
|
+
console.error(`Issue ${missing.join(", ")} not found in current cycle. No ticket claimed.`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
return tasks;
|
|
109
|
+
}
|
|
110
|
+
function decide(task) {
|
|
111
|
+
switch (task.localStatus) {
|
|
112
|
+
case "pending":
|
|
113
|
+
return { task, action: "claim" };
|
|
114
|
+
case "in-progress":
|
|
115
|
+
console.log(`⚠️ 此任務 ${task.id} 已在進行中`);
|
|
116
|
+
return { task, action: "already-in-progress" };
|
|
117
|
+
case "completed":
|
|
118
|
+
console.log(`⚠️ 此任務 ${task.id} 已完成`);
|
|
119
|
+
return { task, action: "skip" };
|
|
120
|
+
case "in-review":
|
|
121
|
+
console.log(`⚠️ 此任務 ${task.id} 正在審核中`);
|
|
122
|
+
return { task, action: "skip" };
|
|
123
|
+
case "blocked":
|
|
124
|
+
console.log(`⚠️ 此任務 ${task.id} 已被阻擋`);
|
|
125
|
+
return { task, action: "skip" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the remote target status once for the whole batch.
|
|
130
|
+
* Returns null when the source is unreachable or not configured, which keeps
|
|
131
|
+
* claiming local-only (e.g. no API key).
|
|
132
|
+
*/
|
|
133
|
+
async function resolveRemoteTarget(config, teamId, inProgressStatus) {
|
|
134
|
+
if (!teamId)
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
const adapter = createAdapter(config);
|
|
138
|
+
const statuses = await adapter.getStatuses(teamId);
|
|
139
|
+
const status = statuses.find((s) => s.name === inProgressStatus);
|
|
140
|
+
return status ? { adapter, status } : null;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Push one ticket to the remote in-progress status. One failing ticket must not
|
|
148
|
+
* abort the batch, and a local/remote divergence has to stay visible.
|
|
149
|
+
*/
|
|
150
|
+
async function pushRemoteStatus(remote, task, statusName, sourceName) {
|
|
151
|
+
const sourceId = task.sourceId ?? task.linearId;
|
|
152
|
+
if (!sourceId)
|
|
153
|
+
return;
|
|
154
|
+
try {
|
|
155
|
+
const result = await remote.adapter.updateIssueStatus(sourceId, remote.status.id);
|
|
156
|
+
if (result.success) {
|
|
157
|
+
task.status = statusName;
|
|
158
|
+
console.log(`${sourceName}: ${task.id} → ${statusName}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(`⚠️ ${sourceName}: ${task.id} 未更新 (${result.error ?? "unknown error"})`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
console.log(`⚠️ ${sourceName}: ${task.id} 未更新 (${message})`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function applyClaims(toClaim, data, config, localConfig) {
|
|
170
|
+
const statusSource = localConfig.status_source || "remote";
|
|
171
|
+
const sourceName = getSourceType(config) === "trello" ? "Trello" : "Linear";
|
|
172
|
+
const transitions = getStatusTransitions(config);
|
|
173
|
+
const remote = statusSource === "remote"
|
|
174
|
+
? await resolveRemoteTarget(config, config.teams[localConfig.team]?.id, transitions.in_progress)
|
|
175
|
+
: null;
|
|
176
|
+
for (const task of toClaim) {
|
|
177
|
+
if (remote) {
|
|
178
|
+
await pushRemoteStatus(remote, task, transitions.in_progress, sourceName);
|
|
179
|
+
}
|
|
180
|
+
task.localStatus = "in-progress";
|
|
181
|
+
console.log(`Local: ${task.id} → in-progress`);
|
|
182
|
+
}
|
|
183
|
+
await saveCycleData(data);
|
|
184
|
+
if (statusSource === "local") {
|
|
185
|
+
console.log(`(${sourceName} status not updated - use 'sync --update' to push)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function resolveIssueIds(positional, pendingTasks) {
|
|
189
|
+
if (positional.length === 0)
|
|
190
|
+
return selectInteractively(pendingTasks);
|
|
191
|
+
if (NEXT_ALIASES.includes(positional[0])) {
|
|
192
|
+
return selectNext(positional[1], pendingTasks);
|
|
193
|
+
}
|
|
194
|
+
return [...new Set(positional)];
|
|
195
|
+
}
|
|
196
|
+
async function claim() {
|
|
197
|
+
const args = process.argv.slice(2);
|
|
198
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
199
|
+
console.log(HELP);
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
const dryRun = args.includes("--dry-run");
|
|
203
|
+
const positional = args.filter((a) => a !== "--dry-run");
|
|
204
|
+
const config = await loadConfig();
|
|
205
|
+
const data = await loadCycleData();
|
|
206
|
+
if (!data) {
|
|
207
|
+
console.error("No cycle data found. Run /sync-linear first.");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const localConfig = await loadLocalConfig();
|
|
211
|
+
const pendingTasks = getPendingTasks(data, config, localConfig);
|
|
212
|
+
const issueIds = await resolveIssueIds(positional, pendingTasks);
|
|
213
|
+
// Availability Check — resolve the whole batch before mutating anything
|
|
214
|
+
const outcomes = resolveTasks(data, issueIds).map(decide);
|
|
215
|
+
const toClaim = pickTasks(outcomes, (a) => a === "claim");
|
|
216
|
+
const toDisplay = pickTasks(outcomes, (a) => a !== "skip");
|
|
217
|
+
if (toDisplay.length === 0) {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
if (toClaim.length > 0 && !dryRun) {
|
|
221
|
+
await applyClaims(toClaim, data, config, localConfig);
|
|
222
|
+
}
|
|
223
|
+
// Display task info
|
|
224
|
+
const icon = dryRun ? "🔍" : "👷";
|
|
225
|
+
for (const task of toDisplay) {
|
|
226
|
+
displayTaskFull(task, icon);
|
|
227
|
+
}
|
|
228
|
+
if (dryRun) {
|
|
229
|
+
console.log("(dry-run: no changes made)");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.log("Next: bun type-check && bun lint");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
claim().catch(console.error);
|
package/dist/scripts/comment.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { createAdapter } from "./lib/adapters/index.js";
|
|
3
|
-
import { getSourceType, loadConfig, loadCycleData, } from "./utils.js";
|
|
3
|
+
import { findTaskByIssueId, getSourceType, loadConfig, loadCycleData, } from "./utils.js";
|
|
4
4
|
function parseArgs(args) {
|
|
5
5
|
let issueId;
|
|
6
6
|
let message;
|
|
@@ -79,7 +79,7 @@ Examples:
|
|
|
79
79
|
issueId = task.id;
|
|
80
80
|
}
|
|
81
81
|
else {
|
|
82
|
-
task = data
|
|
82
|
+
task = data ? findTaskByIssueId(data.tasks, issueId) : undefined;
|
|
83
83
|
if (!task) {
|
|
84
84
|
console.error(`Issue ${issueId} not in local data. Fetching from remote...`);
|
|
85
85
|
task = await fetchTaskFromRemote(issueId, config);
|
package/dist/scripts/done-job.js
CHANGED
|
@@ -7,7 +7,7 @@ import { input, select } from "@inquirer/prompts";
|
|
|
7
7
|
import { handleLinearCompletion, handleTrelloCompletion, parseArgs, printHelp, } from "./lib/done/index.js";
|
|
8
8
|
import { getLatestCommit } from "./lib/git.js";
|
|
9
9
|
import { fetchIssueDetail, syncSingleIssue } from "./lib/sync.js";
|
|
10
|
-
import { getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
10
|
+
import { findTaskByIssueId, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
11
11
|
async function doneJob() {
|
|
12
12
|
const args = process.argv.slice(2);
|
|
13
13
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -80,7 +80,7 @@ async function doneJob() {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
// Find task in local data
|
|
83
|
-
const localTask = data.tasks
|
|
83
|
+
const localTask = findTaskByIssueId(data.tasks, issueId);
|
|
84
84
|
if (!localTask) {
|
|
85
85
|
console.error(`Issue ${issueId} not found in local data.`);
|
|
86
86
|
console.error(`Hint: Run 'ttt sync ${issueId}' or use '--from-remote' flag.`);
|
package/dist/scripts/estimate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { input, select } from "@inquirer/prompts";
|
|
3
|
-
import { loadCycleData, saveCycleData } from "./utils.js";
|
|
3
|
+
import { findTaskByIssueId, loadCycleData, saveCycleData, } from "./utils.js";
|
|
4
4
|
function parseEstimateArgs(args) {
|
|
5
5
|
const parsed = { clear: false };
|
|
6
6
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -31,7 +31,7 @@ function parseEstimateArgs(args) {
|
|
|
31
31
|
}
|
|
32
32
|
function findTask(tasks, issueId) {
|
|
33
33
|
if (issueId) {
|
|
34
|
-
return tasks
|
|
34
|
+
return findTaskByIssueId(tasks, issueId);
|
|
35
35
|
}
|
|
36
36
|
const inProgressTasks = tasks.filter((task) => task.localStatus === "in-progress");
|
|
37
37
|
if (inProgressTasks.length === 1) {
|
|
@@ -67,7 +67,7 @@ export function showPluginInstallInstructions() {
|
|
|
67
67
|
console.log("│ │");
|
|
68
68
|
console.log("│ Available commands after install: │");
|
|
69
69
|
console.log("│ /ttt:sync - Sync Linear issues │");
|
|
70
|
-
console.log("│ /ttt:work-on -
|
|
70
|
+
console.log("│ /ttt:work-on - Claim ticket(s) and work on them │");
|
|
71
71
|
console.log("│ /ttt:done - Complete current task │");
|
|
72
72
|
console.log("│ /ttt:status - Show/modify task status │");
|
|
73
73
|
console.log("│ /ttt:show - Show/search issues │");
|
|
@@ -257,11 +257,11 @@ export async function initLinear(options, paths) {
|
|
|
257
257
|
console.log(` 1. Set ${envName} in your shell profile:`);
|
|
258
258
|
console.log(` export ${envName}="${maskedKey}"`);
|
|
259
259
|
console.log(" 2. Run sync: ttt sync");
|
|
260
|
-
console.log(" 3.
|
|
260
|
+
console.log(" 3. Claim a ticket: ttt claim");
|
|
261
261
|
}
|
|
262
262
|
else {
|
|
263
263
|
console.log(" 1. Run sync: ttt sync");
|
|
264
|
-
console.log(" 2.
|
|
264
|
+
console.log(" 2. Claim a ticket: ttt claim");
|
|
265
265
|
}
|
|
266
266
|
// Show Claude Code plugin installation instructions at the end
|
|
267
267
|
showPluginInstallInstructions();
|
|
@@ -87,7 +87,7 @@ export async function selectStatusSource(options) {
|
|
|
87
87
|
{
|
|
88
88
|
name: "Remote (recommended)",
|
|
89
89
|
value: "remote",
|
|
90
|
-
description: "Update Linear immediately when you
|
|
90
|
+
description: "Update Linear immediately when you claim or complete tasks",
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
name: "Local",
|
|
@@ -252,10 +252,10 @@ export async function initTrello(options, paths) {
|
|
|
252
252
|
console.log(` export TRELLO_API_KEY="${maskedKey}"`);
|
|
253
253
|
console.log(` export TRELLO_TOKEN="<your-token>"`);
|
|
254
254
|
console.log(" 2. Run sync: ttt sync");
|
|
255
|
-
console.log(" 3.
|
|
255
|
+
console.log(" 3. Claim a ticket: ttt claim");
|
|
256
256
|
}
|
|
257
257
|
else {
|
|
258
258
|
console.log(" 1. Run sync: ttt sync");
|
|
259
|
-
console.log(" 2.
|
|
259
|
+
console.log(" 2. Claim a ticket: ttt claim");
|
|
260
260
|
}
|
|
261
261
|
}
|
package/dist/scripts/status.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createAdapter } from "./lib/adapters/index.js";
|
|
|
3
3
|
import { displayTaskWithStatus, getStatusIcon } from "./lib/display.js";
|
|
4
4
|
import { getStatusTransitions, mapLocalStatusToLinear } from "./lib/linear.js";
|
|
5
5
|
import { getFirstTodoStatus } from "./lib/status-helpers.js";
|
|
6
|
-
import { getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
6
|
+
import { findTaskByIssueId, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
7
7
|
const LOCAL_STATUS_ORDER = [
|
|
8
8
|
"pending",
|
|
9
9
|
"in-progress",
|
|
@@ -115,7 +115,7 @@ Examples:
|
|
|
115
115
|
issueId = task.id;
|
|
116
116
|
}
|
|
117
117
|
else {
|
|
118
|
-
task = data.tasks
|
|
118
|
+
task = findTaskByIssueId(data.tasks, issueId);
|
|
119
119
|
if (!task) {
|
|
120
120
|
// Fetch from remote
|
|
121
121
|
console.error(`Issue ${issueId} not in local data. Fetching from remote...`);
|
package/dist/scripts/utils.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { LinearClient } from "@linear/sdk";
|
|
2
2
|
import type { SourceIssue } from "./lib/adapters/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolved on every call, so a TOON_DIR set after this module loads still
|
|
5
|
+
* applies. Caching it at module load made the base directory depend on import
|
|
6
|
+
* order.
|
|
7
|
+
*/
|
|
3
8
|
export declare function getPaths(): {
|
|
4
9
|
baseDir: string;
|
|
5
10
|
configPath: string;
|
|
@@ -153,6 +158,15 @@ export declare function getDefaultTeamKey(config: Config): string;
|
|
|
153
158
|
export declare function getTeamId(config: Config, teamKey?: string): string;
|
|
154
159
|
export declare function getSourceType(config: Config): TaskSourceType;
|
|
155
160
|
export declare function getTaskSourceId(task: Task): string;
|
|
161
|
+
/**
|
|
162
|
+
* Find a task by its display ID.
|
|
163
|
+
*
|
|
164
|
+
* A full ID (e.g. "MP-624") matches exactly. A bare number (e.g. "624") matches
|
|
165
|
+
* the ticket with that number regardless of team prefix; when several teams
|
|
166
|
+
* share the number the candidates are reported and nothing is returned, so the
|
|
167
|
+
* caller asks for the full ID instead of acting on a guess.
|
|
168
|
+
*/
|
|
169
|
+
export declare function findTaskByIssueId(tasks: Task[], issueId: string): Task | undefined;
|
|
156
170
|
export declare function preserveLocalTaskFields(task: Task, existingTask?: Task): Task;
|
|
157
171
|
/**
|
|
158
172
|
* Update or insert a task in cycle data and save to disk.
|
package/dist/scripts/utils.js
CHANGED
|
@@ -17,20 +17,20 @@ function getBaseDir() {
|
|
|
17
17
|
// 3. Default: .ttt directory in current working directory
|
|
18
18
|
return path.join(process.cwd(), ".ttt");
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const ENV_PATH = path.join(BASE_DIR, ".env");
|
|
20
|
+
/**
|
|
21
|
+
* Resolved on every call, so a TOON_DIR set after this module loads still
|
|
22
|
+
* applies. Caching it at module load made the base directory depend on import
|
|
23
|
+
* order.
|
|
24
|
+
*/
|
|
26
25
|
export function getPaths() {
|
|
26
|
+
const baseDir = getBaseDir();
|
|
27
27
|
return {
|
|
28
|
-
baseDir
|
|
29
|
-
configPath:
|
|
30
|
-
cyclePath:
|
|
31
|
-
localPath:
|
|
32
|
-
outputPath:
|
|
33
|
-
envPath:
|
|
28
|
+
baseDir,
|
|
29
|
+
configPath: path.join(baseDir, "config.toon"),
|
|
30
|
+
cyclePath: path.join(baseDir, "cycle.toon"),
|
|
31
|
+
localPath: path.join(baseDir, "local.toon"),
|
|
32
|
+
outputPath: path.join(baseDir, "output"),
|
|
33
|
+
envPath: path.join(baseDir, ".env"),
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
// Linear priority value to name mapping (fixed by Linear API)
|
|
@@ -64,23 +64,25 @@ export async function fileExists(filePath) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
export async function loadConfig() {
|
|
67
|
+
const { configPath } = getPaths();
|
|
67
68
|
try {
|
|
68
|
-
const fileContent = await fs.readFile(
|
|
69
|
+
const fileContent = await fs.readFile(configPath, "utf-8");
|
|
69
70
|
return decode(fileContent, { strict: false });
|
|
70
71
|
}
|
|
71
72
|
catch (error) {
|
|
72
|
-
console.error(`Error loading config from ${
|
|
73
|
+
console.error(`Error loading config from ${configPath}:`, error);
|
|
73
74
|
console.error("Run `bun run init` to create configuration files.");
|
|
74
75
|
process.exit(1);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
export async function loadLocalConfig() {
|
|
79
|
+
const { localPath } = getPaths();
|
|
78
80
|
try {
|
|
79
|
-
const fileContent = await fs.readFile(
|
|
81
|
+
const fileContent = await fs.readFile(localPath, "utf-8");
|
|
80
82
|
return decode(fileContent, { strict: false });
|
|
81
83
|
}
|
|
82
84
|
catch {
|
|
83
|
-
console.error(`Error: ${
|
|
85
|
+
console.error(`Error: ${localPath} not found.`);
|
|
84
86
|
console.error("Run `bun run init` to create local configuration.");
|
|
85
87
|
process.exit(1);
|
|
86
88
|
}
|
|
@@ -174,32 +176,33 @@ export async function withRetry(fn, opts = {}) {
|
|
|
174
176
|
throw lastErr;
|
|
175
177
|
}
|
|
176
178
|
export async function loadCycleData() {
|
|
179
|
+
const { cyclePath } = getPaths();
|
|
177
180
|
try {
|
|
178
|
-
await fs.access(
|
|
181
|
+
await fs.access(cyclePath);
|
|
179
182
|
}
|
|
180
183
|
catch {
|
|
181
184
|
return null;
|
|
182
185
|
}
|
|
183
186
|
try {
|
|
184
|
-
const fileContent = await fs.readFile(
|
|
187
|
+
const fileContent = await fs.readFile(cyclePath, "utf-8");
|
|
185
188
|
return decode(fileContent, { strict: false });
|
|
186
189
|
}
|
|
187
190
|
catch (error) {
|
|
188
|
-
console.error(`Warning: Failed to decode ${
|
|
191
|
+
console.error(`Warning: Failed to decode ${cyclePath}: ${error instanceof Error ? error.message : error}`);
|
|
189
192
|
return null;
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
export async function saveCycleData(data) {
|
|
193
196
|
const toonString = encode(data);
|
|
194
|
-
await fs.writeFile(
|
|
197
|
+
await fs.writeFile(getPaths().cyclePath, toonString, "utf-8");
|
|
195
198
|
}
|
|
196
199
|
export async function saveConfig(config) {
|
|
197
200
|
const toonString = encode(config);
|
|
198
|
-
await fs.writeFile(
|
|
201
|
+
await fs.writeFile(getPaths().configPath, toonString, "utf-8");
|
|
199
202
|
}
|
|
200
203
|
export async function saveLocalConfig(config) {
|
|
201
204
|
const toonString = encode(config);
|
|
202
|
-
await fs.writeFile(
|
|
205
|
+
await fs.writeFile(getPaths().localPath, toonString, "utf-8");
|
|
203
206
|
}
|
|
204
207
|
// Get first team key from config
|
|
205
208
|
export function getDefaultTeamKey(config) {
|
|
@@ -229,6 +232,28 @@ export function getSourceType(config) {
|
|
|
229
232
|
export function getTaskSourceId(task) {
|
|
230
233
|
return task.sourceId ?? task.linearId;
|
|
231
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Find a task by its display ID.
|
|
237
|
+
*
|
|
238
|
+
* A full ID (e.g. "MP-624") matches exactly. A bare number (e.g. "624") matches
|
|
239
|
+
* the ticket with that number regardless of team prefix; when several teams
|
|
240
|
+
* share the number the candidates are reported and nothing is returned, so the
|
|
241
|
+
* caller asks for the full ID instead of acting on a guess.
|
|
242
|
+
*/
|
|
243
|
+
export function findTaskByIssueId(tasks, issueId) {
|
|
244
|
+
const exact = tasks.find((t) => t.id === issueId);
|
|
245
|
+
if (exact)
|
|
246
|
+
return exact;
|
|
247
|
+
if (!/^\d+$/.test(issueId))
|
|
248
|
+
return undefined;
|
|
249
|
+
const matches = tasks.filter((t) => t.id.endsWith(`-${issueId}`));
|
|
250
|
+
if (matches.length === 1)
|
|
251
|
+
return matches[0];
|
|
252
|
+
if (matches.length > 1) {
|
|
253
|
+
console.error(`Ambiguous issue "${issueId}": ${matches.map((t) => t.id).join(", ")}. Use the full ID.`);
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
232
257
|
export function preserveLocalTaskFields(task, existingTask) {
|
|
233
258
|
if (!existingTask) {
|
|
234
259
|
return task;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "team-toon-tack",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
4
4
|
"description": "Linear & Trello task sync & management CLI with TOON format",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"lint": "biome lint .",
|
|
24
24
|
"format": "biome check --write .",
|
|
25
25
|
"type": "tsc --noEmit",
|
|
26
|
+
"test": "bun test",
|
|
26
27
|
"prepublishOnly": "npm run build",
|
|
27
28
|
"release": "npm config set //registry.npmjs.org/:_authToken=$NPM_PUBLISH_KEY && npm publish"
|
|
28
29
|
},
|
|
@@ -23,7 +23,7 @@ Manage developer task workflows using the `ttt` CLI.
|
|
|
23
23
|
|-------------|---------|---------|
|
|
24
24
|
| Sync/fetch issues | `ttt sync` | "sync my issues", "pull from Linear" |
|
|
25
25
|
| Show/search issues | `ttt show` | "show MP-624", "list my tasks", "what issues do I have" |
|
|
26
|
-
|
|
|
26
|
+
| Claim ticket(s) → in-progress | `ttt claim` | "領票", "work on next", "start MP-624", "claim MP-624 MP-625" |
|
|
27
27
|
| Create a new issue | `ttt create` | "create issue", "open a ticket", "new task" |
|
|
28
28
|
| Reassign an issue | `ttt assign` | "assign MP-624 to john", "reassign to jane" |
|
|
29
29
|
| Edit issue fields | `ttt edit` | "rename MP-624", "change priority", "update labels" |
|
|
@@ -44,8 +44,9 @@ ttt sync MP-624 # Sync specific issue
|
|
|
44
44
|
ttt show # List all local issues
|
|
45
45
|
ttt show MP-624 # Show issue details
|
|
46
46
|
ttt show --user me # My issues
|
|
47
|
-
ttt
|
|
48
|
-
ttt
|
|
47
|
+
ttt claim next # Claim highest priority pending task
|
|
48
|
+
ttt claim next 3 # Claim top 3 pending tasks
|
|
49
|
+
ttt claim MP-624 MP-625 # Batch claim (alias: ttt work-on)
|
|
49
50
|
ttt create # Create new issue (interactive)
|
|
50
51
|
ttt create -t "Title" -p 2 # Quick create with flags
|
|
51
52
|
ttt assign MP-624 -a john # Reassign issue
|
|
@@ -68,7 +69,7 @@ ttt done -m "summary" # Complete with message
|
|
|
68
69
|
## Standard Workflow
|
|
69
70
|
|
|
70
71
|
```
|
|
71
|
-
ttt sync → ttt
|
|
72
|
+
ttt sync → ttt claim next → ttt estimate <id> <hours> → [implement] → git commit → ttt comment -m "notes" → ttt done -m "..."
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
## File Structure
|
|
@@ -112,7 +113,7 @@ digraph ttt_router {
|
|
|
112
113
|
|
|
113
114
|
sync [label="ttt sync", shape=box];
|
|
114
115
|
show [label="ttt show", shape=box];
|
|
115
|
-
|
|
116
|
+
claim [label="ttt claim", shape=box];
|
|
116
117
|
create [label="ttt create", shape=box];
|
|
117
118
|
assign [label="ttt assign", shape=box];
|
|
118
119
|
edit [label="ttt edit", shape=box];
|
|
@@ -125,7 +126,7 @@ digraph ttt_router {
|
|
|
125
126
|
user -> classify;
|
|
126
127
|
classify -> sync [label="fetch / pull"];
|
|
127
128
|
classify -> show [label="show / list"];
|
|
128
|
-
classify ->
|
|
129
|
+
classify -> claim [label="claim / start / next"];
|
|
129
130
|
classify -> create [label="create / new"];
|
|
130
131
|
classify -> assign [label="assign / reassign"];
|
|
131
132
|
classify -> edit [label="rename / change field"];
|
package/dist/scripts/work-on.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { select } from "@inquirer/prompts";
|
|
2
|
-
import { createAdapter } from "./lib/adapters/index.js";
|
|
3
|
-
import { displayTaskFull, PRIORITY_LABELS } from "./lib/display.js";
|
|
4
|
-
import { getStatusTransitions } from "./lib/linear.js";
|
|
5
|
-
import { getPrioritySortIndex, getSourceType, loadConfig, loadCycleData, loadLocalConfig, saveCycleData, } from "./utils.js";
|
|
6
|
-
async function workOn() {
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
9
|
-
console.log(`Usage: ttt work-on [issue-id] [options]
|
|
10
|
-
|
|
11
|
-
Arguments:
|
|
12
|
-
issue-id Issue ID (e.g., MP-624) or 'next' for auto-select
|
|
13
|
-
If omitted, shows interactive selection
|
|
14
|
-
|
|
15
|
-
Options:
|
|
16
|
-
--dry-run Pick task without changing status (preview only)
|
|
17
|
-
|
|
18
|
-
Examples:
|
|
19
|
-
ttt work-on # Interactive selection
|
|
20
|
-
ttt work-on MP-624 # Work on specific issue
|
|
21
|
-
ttt work-on next # Auto-select highest priority
|
|
22
|
-
ttt work-on --dry-run # Preview selection without changes`);
|
|
23
|
-
process.exit(0);
|
|
24
|
-
}
|
|
25
|
-
const dryRun = args.includes("--dry-run");
|
|
26
|
-
const filteredArgs = args.filter((a) => a !== "--dry-run");
|
|
27
|
-
let issueId = filteredArgs[0];
|
|
28
|
-
const config = await loadConfig();
|
|
29
|
-
const data = await loadCycleData();
|
|
30
|
-
if (!data) {
|
|
31
|
-
console.error("No cycle data found. Run /sync-linear first.");
|
|
32
|
-
process.exit(1);
|
|
33
|
-
}
|
|
34
|
-
const localConfig = await loadLocalConfig();
|
|
35
|
-
// Get current user email(s) for filtering
|
|
36
|
-
const userKeys = Array.isArray(localConfig.current_user)
|
|
37
|
-
? localConfig.current_user
|
|
38
|
-
: localConfig.current_user
|
|
39
|
-
? [localConfig.current_user]
|
|
40
|
-
: [];
|
|
41
|
-
const currentUserEmails = userKeys
|
|
42
|
-
.map((key) => config.users[key]?.email?.toLowerCase())
|
|
43
|
-
.filter((email) => !!email);
|
|
44
|
-
const pendingTasks = data.tasks
|
|
45
|
-
.filter((t) => {
|
|
46
|
-
if (t.localStatus !== "pending")
|
|
47
|
-
return false;
|
|
48
|
-
if (currentUserEmails.length === 0)
|
|
49
|
-
return true; // No filter = all users
|
|
50
|
-
if (!t.assignee)
|
|
51
|
-
return false;
|
|
52
|
-
return currentUserEmails.includes(t.assignee.toLowerCase());
|
|
53
|
-
})
|
|
54
|
-
.sort((a, b) => {
|
|
55
|
-
const pa = getPrioritySortIndex(a.priority, config.priority_order);
|
|
56
|
-
const pb = getPrioritySortIndex(b.priority, config.priority_order);
|
|
57
|
-
return pa - pb;
|
|
58
|
-
});
|
|
59
|
-
// Issue Resolution
|
|
60
|
-
if (!issueId) {
|
|
61
|
-
if (pendingTasks.length === 0) {
|
|
62
|
-
console.log("✅ 沒有待處理的任務,所有工作已完成或進行中");
|
|
63
|
-
process.exit(0);
|
|
64
|
-
}
|
|
65
|
-
const choices = pendingTasks.map((task) => ({
|
|
66
|
-
name: `${PRIORITY_LABELS[task.priority] || "⚪"} ${task.id}: ${task.title}`,
|
|
67
|
-
value: task.id,
|
|
68
|
-
description: task.labels.join(", "),
|
|
69
|
-
}));
|
|
70
|
-
const selectedId = await select({
|
|
71
|
-
message: "選擇要處理的任務:",
|
|
72
|
-
choices: choices,
|
|
73
|
-
});
|
|
74
|
-
if (!selectedId) {
|
|
75
|
-
console.log("已取消");
|
|
76
|
-
process.exit(0);
|
|
77
|
-
}
|
|
78
|
-
issueId = selectedId;
|
|
79
|
-
}
|
|
80
|
-
else if (["next", "下一個", "下一個工作"].includes(issueId)) {
|
|
81
|
-
if (pendingTasks.length === 0) {
|
|
82
|
-
console.log("✅ 沒有待處理的任務,所有工作已完成或進行中");
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
issueId = pendingTasks[0].id;
|
|
86
|
-
console.log(`Auto-selected: ${issueId}`);
|
|
87
|
-
}
|
|
88
|
-
// Find task
|
|
89
|
-
const task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
|
|
90
|
-
if (!task) {
|
|
91
|
-
console.error(`Issue ${issueId} not found in current cycle.`);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
// Availability Check
|
|
95
|
-
if (task.localStatus === "in-progress") {
|
|
96
|
-
console.log(`⚠️ 此任務 ${task.id} 已在進行中`);
|
|
97
|
-
}
|
|
98
|
-
else if (task.localStatus === "completed") {
|
|
99
|
-
console.log(`⚠️ 此任務 ${task.id} 已完成`);
|
|
100
|
-
process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
else if (task.localStatus === "in-review") {
|
|
103
|
-
console.log(`⚠️ 此任務 ${task.id} 正在審核中`);
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}
|
|
106
|
-
// Mark as In Progress (skip if dry-run)
|
|
107
|
-
if (task.localStatus === "pending" && !dryRun) {
|
|
108
|
-
task.localStatus = "in-progress";
|
|
109
|
-
// Update remote (only if status_source is 'remote' or not set)
|
|
110
|
-
const statusSource = localConfig.status_source || "remote";
|
|
111
|
-
const sourceType = getSourceType(config);
|
|
112
|
-
const sourceId = task.sourceId ?? task.linearId;
|
|
113
|
-
if (statusSource === "remote" && sourceId) {
|
|
114
|
-
const transitions = getStatusTransitions(config);
|
|
115
|
-
try {
|
|
116
|
-
const adapter = createAdapter(config);
|
|
117
|
-
// Get statuses to find status ID
|
|
118
|
-
const teamId = config.teams[localConfig.team]?.id;
|
|
119
|
-
if (teamId) {
|
|
120
|
-
const statuses = await adapter.getStatuses(teamId);
|
|
121
|
-
const targetStatus = statuses.find((s) => s.name === transitions.in_progress);
|
|
122
|
-
if (targetStatus) {
|
|
123
|
-
const result = await adapter.updateIssueStatus(sourceId, targetStatus.id);
|
|
124
|
-
if (result.success) {
|
|
125
|
-
task.status = transitions.in_progress;
|
|
126
|
-
const sourceName = sourceType === "trello" ? "Trello" : "Linear";
|
|
127
|
-
console.log(`${sourceName}: ${task.id} → ${transitions.in_progress}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (_error) {
|
|
133
|
-
// Silently fail if adapter not available (e.g., no API key)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
await saveCycleData(data);
|
|
137
|
-
console.log(`Local: ${task.id} → in-progress`);
|
|
138
|
-
if (statusSource === "local") {
|
|
139
|
-
const sourceName = sourceType === "trello" ? "Trello" : "Linear";
|
|
140
|
-
console.log(`(${sourceName} status not updated - use 'sync --update' to push)`);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Display task info
|
|
144
|
-
const icon = dryRun ? "🔍" : "👷";
|
|
145
|
-
displayTaskFull(task, icon);
|
|
146
|
-
if (dryRun) {
|
|
147
|
-
console.log("(dry-run: no changes made)");
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
console.log("Next: bun type-check && bun lint");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
workOn().catch(console.error);
|
|
File without changes
|