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.
@@ -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.9.1"
16
+ "version": "3.10.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.9.1",
4
+ "version": "3.10.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.9.1",
3
+ "version": "3.10.0",
4
4
  "description": "透過 ttt CLI 同步與管理 Linear、Trello 任務。",
5
5
  "author": {
6
6
  "name": "wayne930242"
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
- - **Smart Task Selection** — Auto-pick your highest priority pending task with `/work-on next`
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 # Pick highest priority task & start working
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 work-on next
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 work-on`
147
+ ### `ttt claim`
148
148
 
149
- Start working on a task.
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 work-on # Interactive selection
153
- ttt work-on MP-123 # Specific issue
154
- ttt work-on next # Auto-select highest priority
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` | Start working on a task |
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
- - **智慧任務挑選**`/work-on next` 自動選擇指派給你的最高優先級待辦任務
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 work-on next
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 work-on`
147
+ ### `ttt claim`
148
148
 
149
- 開始處理任務。
149
+ 領票:把票在本地與遠端來源改成 in-progress。
150
+ 別名:`ttt work-on`。
150
151
 
151
152
  ```bash
152
- ttt work-on # 互動選擇
153
- ttt work-on MP-123 # 指定 issue
154
- ttt work-on next # 自動選擇最高優先級
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` | 顯示或修改任務狀態 |
@@ -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 work-on` first |
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 |
@@ -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 work-on` first |
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 |
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: ttt:work-on
3
- description: Start working on a Linear task
3
+ description: Claim Linear/Trello ticket(s) and work on them
4
4
  arguments:
5
- - name: issue-id
6
- description: "Issue ID (e.g., MP-624) or 'next' for auto-select. Defaults to 'next'."
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 work-on` command using the Bash tool.
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 the task is completed, YOU MUST execute `/ttt:done -m "summary"`. This is MANDATORY.
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 — Start Working on a Task
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 work-on {{ issue-id | default: "next" }} {{ "--dry-run" if dry-run }}
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 work-on next` |
32
- | `next` | `ttt work-on next` |
33
- | `MP-624` | `ttt work-on MP-624` |
34
- | `--dry-run` | `ttt work-on next --dry-run` |
35
- | `MP-624 --dry-run` | `ttt work-on MP-624 --dry-run` |
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 work-on [issue-id] [options]
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 Issue ID (e.g., MP-624) or 'next' for auto-select
44
- If omitted, shows interactive selection
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 task without changing status (preview only)
57
+ --dry-run Pick tickets without changing status (preview only)
48
58
  ```
49
59
 
50
- ## After Selection Delegate the Implementation
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 picks the task. The implementation workflow belongs to the user.
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
- Either path closes the task with `/ttt:done -m "summary"`.
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 work-on picks a task.
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
- work-on Start working on a task (interactive or by ID)
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 work-on # Interactive task selection
62
- ttt work-on MP-123 # Work on specific issue
63
- ttt work-on next # Auto-select highest priority
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", "work-on.js", ...commandArgs];
151
- await importScript("work-on.js");
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);
@@ -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?.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
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);
@@ -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.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
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.`);
@@ -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.find((task) => task.id === issueId || task.id === `MP-${issueId}`);
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 - Start working on a task │");
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. Start working: ttt work-on");
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. Start working: ttt work-on");
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 work-on or complete tasks",
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. Start working: ttt work-on");
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. Start working: ttt work-on");
259
+ console.log(" 2. Claim a ticket: ttt claim");
260
260
  }
261
261
  }
@@ -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.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
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...`);
@@ -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.
@@ -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
- const BASE_DIR = getBaseDir();
21
- const CONFIG_PATH = path.join(BASE_DIR, "config.toon");
22
- const CYCLE_PATH = path.join(BASE_DIR, "cycle.toon");
23
- const LOCAL_PATH = path.join(BASE_DIR, "local.toon");
24
- const OUTPUT_PATH = path.join(BASE_DIR, "output");
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: BASE_DIR,
29
- configPath: CONFIG_PATH,
30
- cyclePath: CYCLE_PATH,
31
- localPath: LOCAL_PATH,
32
- outputPath: OUTPUT_PATH,
33
- envPath: ENV_PATH,
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(CONFIG_PATH, "utf-8");
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 ${CONFIG_PATH}:`, error);
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(LOCAL_PATH, "utf-8");
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: ${LOCAL_PATH} not found.`);
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(CYCLE_PATH);
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(CYCLE_PATH, "utf-8");
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 ${CYCLE_PATH}: ${error instanceof Error ? error.message : error}`);
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(CYCLE_PATH, toonString, "utf-8");
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(CONFIG_PATH, toonString, "utf-8");
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(LOCAL_PATH, toonString, "utf-8");
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.9.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
- | Start working on a task | `ttt work-on` | "work on next", "start MP-624" |
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 work-on next # Auto-select highest priority
48
- ttt work-on MP-624 # Start specific task
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 work-on next → ttt estimate <id> <hours> → [implement] → git commit → ttt comment -m "notes" → ttt done -m "..."
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
- work_on [label="ttt work-on", shape=box];
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 -> work_on [label="start / next"];
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"];
@@ -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