team-toon-tack 3.7.7 → 3.9.3

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": "2.8.5"
16
+ "version": "3.9.1"
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": "2.8.5",
4
+ "version": "3.9.1",
5
5
  "author": {
6
6
  "name": "wayne930242",
7
7
  "email": "wayne930242@gmail.com"
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "team-toon-tack",
3
+ "version": "3.9.1",
4
+ "description": "透過 ttt CLI 同步與管理 Linear、Trello 任務。",
5
+ "author": {
6
+ "name": "wayne930242"
7
+ },
8
+ "repository": "https://github.com/wayne930242/team-toon-tack",
9
+ "license": "MIT",
10
+ "skills": "./skills/",
11
+ "interface": {
12
+ "displayName": "team-toon-tack",
13
+ "shortDescription": "同步與管理 Linear、Trello 任務",
14
+ "longDescription": "透過 ttt CLI 同步任務、查詢狀態、記錄估時,並依使用者要求更新 Linear 與 Trello。",
15
+ "developerName": "wayne930242",
16
+ "category": "Productivity",
17
+ "capabilities": ["Interactive", "Write"],
18
+ "defaultPrompt": ["使用 ttt 同步並列出我的任務。"]
19
+ }
20
+ }
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 highest priority unassigned work with `/work-on next`
11
+ - **Smart Task Selection** — Auto-pick your highest priority pending task with `/work-on next`
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
@@ -67,9 +67,32 @@ During init, you'll be prompted to select your task source (Linear or Trello) an
67
67
 
68
68
  ```
69
69
  /plugin marketplace add wayne930242/team-toon-tack
70
- /plugin install team-toon-tack@wayne930242
70
+ /plugin install team-toon-tack@ttt-marketplace
71
71
  ```
72
72
 
73
+ ### Codex plugin 安裝(選擇性)
74
+
75
+ 先依上方步驟安裝 `ttt` CLI 並執行 `ttt init`。Plugin 提供任務管理技能,透過 shell 呼叫 CLI。
76
+
77
+ 從 GitHub 安裝:
78
+
79
+ ```bash
80
+ codex plugin marketplace add wayne930242/team-toon-tack
81
+ codex plugin add team-toon-tack@ttt-marketplace
82
+ ```
83
+
84
+ 從本地 checkout 安裝,在此 repository 根目錄執行:
85
+
86
+ ```bash
87
+ codex plugin marketplace add "$PWD"
88
+ codex plugin add team-toon-tack@ttt-marketplace
89
+ ```
90
+
91
+ 若已註冊同名的 GitHub marketplace,先執行 `codex plugin marketplace remove ttt-marketplace`,再加入本地路徑。
92
+ 修改已安裝的同版本內容後,先執行 `codex plugin remove team-toon-tack@ttt-marketplace`,再執行上述 `codex plugin add` 重新安裝。
93
+
94
+ 安裝完成後開啟新的 Codex 對話,使用 `$managing-linear-tasks`,或直接說「使用 ttt 同步並列出我的任務」。Codex 會使用 `ttt sync`、`ttt show` 等 CLI 指令。
95
+
73
96
  ### 3. Daily Workflow
74
97
 
75
98
  In Claude Code (with plugin installed):
@@ -111,6 +134,9 @@ ttt init --force # Overwrite existing config
111
134
 
112
135
  Sync current cycle issues from Linear/Trello.
113
136
 
137
+ Only issues assigned to `current_user` are synced. Leave `current_user` empty in
138
+ `.ttt/local.toon` for a team-wide sync.
139
+
114
140
  ```bash
115
141
  ttt sync # Sync Todo/In Progress issues (fast)
116
142
  ttt sync --all # Sync all issues regardless of status
@@ -234,7 +260,7 @@ Install the plugin for Claude Code integration:
234
260
 
235
261
  ```
236
262
  /plugin marketplace add wayne930242/team-toon-tack
237
- /plugin install team-toon-tack@wayne930242
263
+ /plugin install team-toon-tack@ttt-marketplace
238
264
  ```
239
265
 
240
266
  ### Available Commands
@@ -250,7 +276,7 @@ Install the plugin for Claude Code integration:
250
276
 
251
277
  ### Auto-Activated Skill
252
278
 
253
- The plugin includes a `linear-task-manager` skill that automatically activates when working with tasks, providing workflow guidance and best practices.
279
+ The plugin includes a `managing-linear-tasks` skill that automatically activates when working with tasks, providing workflow guidance and best practices.
254
280
 
255
281
  ## License
256
282
 
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
+ - **智慧任務挑選** — `/work-on next` 自動選擇指派給你的最高優先級待辦任務
12
12
  - **多團隊支援** — 跨多個團隊/看板同步與過濾 issue
13
13
  - **彈性同步模式** — 選擇 remote(即時同步)或 local(離線優先,稍後用 `--update` 同步)
14
14
  - **完成模式** — 四種任務完成模式(Linear):簡單、嚴格審查、上下游嚴格、上下游非嚴格
@@ -67,9 +67,32 @@ ttt init
67
67
 
68
68
  ```
69
69
  /plugin marketplace add wayne930242/team-toon-tack
70
- /plugin install team-toon-tack@wayne930242
70
+ /plugin install team-toon-tack@ttt-marketplace
71
71
  ```
72
72
 
73
+ ### Codex plugin 安裝(選擇性)
74
+
75
+ 先依上方步驟安裝 `ttt` CLI 並執行 `ttt init`。Plugin 提供任務管理技能,透過 shell 呼叫 CLI。
76
+
77
+ 從 GitHub 安裝:
78
+
79
+ ```bash
80
+ codex plugin marketplace add wayne930242/team-toon-tack
81
+ codex plugin add team-toon-tack@ttt-marketplace
82
+ ```
83
+
84
+ 從本地 checkout 安裝,在此 repository 根目錄執行:
85
+
86
+ ```bash
87
+ codex plugin marketplace add "$PWD"
88
+ codex plugin add team-toon-tack@ttt-marketplace
89
+ ```
90
+
91
+ 若已註冊同名的 GitHub marketplace,先執行 `codex plugin marketplace remove ttt-marketplace`,再加入本地路徑。
92
+ 修改已安裝的同版本內容後,先執行 `codex plugin remove team-toon-tack@ttt-marketplace`,再執行上述 `codex plugin add` 重新安裝。
93
+
94
+ 安裝完成後開啟新的 Codex 對話,使用 `$managing-linear-tasks`,或直接說「使用 ttt 同步並列出我的任務」。Codex 會使用 `ttt sync`、`ttt show` 等 CLI 指令。
95
+
73
96
  ### 3. 每日工作流
74
97
 
75
98
  在 Claude Code 中(安裝 plugin 後):
@@ -111,6 +134,9 @@ ttt init --force # 覆蓋現有配置
111
134
 
112
135
  從 Linear/Trello 同步當前 cycle 的 issue。
113
136
 
137
+ 只同步指派給 `current_user` 的 issue。若要同步整個團隊,把 `.ttt/local.toon` 的
138
+ `current_user` 留空。
139
+
114
140
  ```bash
115
141
  ttt sync # 同步 Todo/In Progress 狀態的 issue(較快)
116
142
  ttt sync --all # 同步所有狀態的 issue
@@ -234,7 +260,7 @@ your-project/
234
260
 
235
261
  ```
236
262
  /plugin marketplace add wayne930242/team-toon-tack
237
- /plugin install team-toon-tack@wayne930242
263
+ /plugin install team-toon-tack@ttt-marketplace
238
264
  ```
239
265
 
240
266
  ### 可用指令
@@ -250,7 +276,7 @@ your-project/
250
276
 
251
277
  ### 自動啟用技能
252
278
 
253
- Plugin 包含 `linear-task-manager` 技能,在處理任務時會自動啟用,提供工作流程指導和最佳實踐。
279
+ Plugin 包含 `managing-linear-tasks` 技能,在處理任務時會自動啟用,提供工作流程指導和最佳實踐。
254
280
 
255
281
  ## 授權
256
282
 
@@ -5,7 +5,6 @@ arguments:
5
5
  - name: issue-id
6
6
  description: "Issue ID (e.g., MP-624) or 'next' for auto-select. Defaults to 'next'."
7
7
  required: false
8
- default: next
9
8
  - name: dry-run
10
9
  description: Preview selection without changing status
11
10
  required: false
@@ -48,67 +47,21 @@ Options:
48
47
  --dry-run Pick task without changing status (preview only)
49
48
  ```
50
49
 
51
- ## WorkflowPlan Test → Code → Review
50
+ ## After Selection Delegate the Implementation
52
51
 
53
- Follow this loop for every task.
54
- Skipping phases is not "pragmatic", it is debt.
52
+ This command picks the task. The implementation workflow belongs to the user.
55
53
 
56
- ### 1. Branch
54
+ **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.
57
55
 
58
- Check current branch and repo convention first (e.g. recent `git log --oneline -10`, existing branch naming pattern).
59
- If unclear whether to branch or what name to use, ask the user instead of assuming.
56
+ **Only when none exists**, run this minimal loop:
60
57
 
61
- ```bash
62
- git checkout -b <branch-name-matching-user-convention>
63
- ```
64
-
65
- ### 2. Plan
66
-
67
- Scope decides depth.
68
-
69
- - Unclear requirements or 3+ files touched → if your environment has a brainstorming/planning skill, use it; otherwise clarify scope with the user and write a short plan before coding.
70
- - Clear and small (≤2 files, obvious change) → state a 2–3 bullet plan inline before coding.
71
-
72
- Never go straight to code.
73
- Planning collapses ambiguity before implementation.
74
-
75
- ### 3. Test First (TDD)
76
-
77
- Red → Green → Refactor per behavior:
78
-
79
- 1. Write one failing test naming the behavior.
80
- 2. Run test — verify it fails for the expected reason.
81
- 3. Write minimal code to pass.
82
- 4. Run test — verify pass, other tests still green.
83
- 5. Refactor if needed, keep green.
84
-
85
- No production code without a failing test first.
86
-
87
- ### 4. Review (before `/ttt:done`)
88
-
89
- Verify before claiming completion — run and confirm output:
90
-
91
- - Full test suite passes.
92
- - `npm run lint` / `npm run type` clean (or project-specific commands).
93
- - Only requested scope changed — no drive-by edits.
94
- - No debug logs, stubs, TODOs.
95
-
96
- Evidence before assertions.
97
- Do not claim complete without running the commands.
98
-
99
- ### 5. Complete
100
-
101
- ```text
102
- /ttt:done -m "summary"
103
- ```
104
-
105
- A task is NOT complete until `/ttt:done` runs.
106
- This is MANDATORY.
107
-
108
- ## Project-Specific Skill
58
+ - Branch using the repo's existing naming convention.
59
+ - State a short plan before coding; for unclear scope or 3+ files, settle the scope with the user first.
60
+ - Write the failing test first, then the minimal code to pass it.
61
+ - Run lint / type / test and report the real output before claiming completion.
62
+ - Offer `/ttt:write-work-on-skill` to capture this project's commands as a reusable skill.
109
63
 
110
- Check `.claude/skills/work-on/` or `.claude/skills/start-work/` for project-specific lint / type / test commands and conventions.
111
- If missing, suggest `/ttt:write-work-on-skill` — it scaffolds a skill following this same 4-phase structure.
64
+ Either path closes the task with `/ttt:done -m "summary"`.
112
65
 
113
66
  ## Error Handling
114
67
 
package/dist/bin/cli.js CHANGED
@@ -172,7 +172,7 @@ async function main() {
172
172
  break;
173
173
  case "create":
174
174
  process.argv = ["node", "create.js", ...commandArgs];
175
- await importScript("create.js");
175
+ await importScript("create.js").then((module) => module.create());
176
176
  break;
177
177
  case "assign":
178
178
  process.argv = ["node", "assign.js", ...commandArgs];
@@ -70,4 +70,7 @@ Examples:
70
70
  console.log(` Removed from local cycle data.`);
71
71
  }
72
72
  }
73
- cancel().catch(console.error);
73
+ cancel().catch((error) => {
74
+ console.error(error);
75
+ process.exit(1);
76
+ });
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env bun
2
- export {};
2
+ export declare function create(args?: string[]): Promise<void>;
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bun
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
2
4
  import { input, select } from "@inquirer/prompts";
3
5
  import { createAdapter } from "./lib/adapters/index.js";
4
6
  import { getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, sourceIssueToTask, upsertTaskInCycleData, } from "./utils.js";
@@ -121,8 +123,7 @@ async function resolveParentSourceId(adapter, parentIdentifier) {
121
123
  }
122
124
  return parent.sourceId;
123
125
  }
124
- async function create() {
125
- const args = process.argv.slice(2);
126
+ export async function create(args = process.argv.slice(2)) {
126
127
  if (args.includes("--help") || args.includes("-h")) {
127
128
  console.log(`Usage: ttt create [options]
128
129
 
@@ -156,6 +157,10 @@ Examples:
156
157
  const labelArg = parsed.label;
157
158
  const statusName = parsed.status;
158
159
  const parentId = parsed.parent;
160
+ if (sourceType === "trello" && parentId) {
161
+ console.error("Parent issues are not supported for Trello cards.");
162
+ process.exit(1);
163
+ }
159
164
  if (!title) {
160
165
  if (!parsed.interactive) {
161
166
  console.error("Title is required when --no-interactive is set.");
@@ -204,8 +209,13 @@ Examples:
204
209
  }
205
210
  console.log("Creating issue...");
206
211
  const adapter = createAdapter(config);
212
+ let cycleData = sourceType === "trello" ? await loadCycleData() : null;
213
+ if (sourceType === "trello" && !cycleData) {
214
+ console.error("No cycle data found. Run ttt sync before creating a card.");
215
+ process.exit(1);
216
+ }
207
217
  const assigneeId = resolveAssigneeId(config, assigneeKey);
208
- const statusId = await resolveStatusId(config, adapter, teamId, statusName);
218
+ const statusId = await resolveStatusId(config, adapter, teamId, sourceType === "trello" && !statusName ? "todo" : statusName);
209
219
  const labelIds = resolveLabelIds(config, labelArg);
210
220
  const parentSourceId = await resolveParentSourceId(adapter, parentId);
211
221
  const currentCycle = await adapter.getCurrentCycle(teamId);
@@ -229,11 +239,19 @@ Examples:
229
239
  console.log(`\n✅ Created ${issue.id}: ${issue.title}`);
230
240
  if (issue.url)
231
241
  console.log(` ${issue.url}`);
232
- const cycleData = await loadCycleData();
242
+ if (sourceType !== "trello")
243
+ cycleData = await loadCycleData();
233
244
  if (cycleData) {
234
245
  const task = sourceIssueToTask(issue, sourceType);
235
246
  await upsertTaskInCycleData(task, cycleData);
236
247
  console.log(` Added to local cycle data.`);
237
248
  }
238
249
  }
239
- create().catch(console.error);
250
+ const entryPath = process.argv[1];
251
+ if (entryPath &&
252
+ import.meta.url === pathToFileURL(path.resolve(entryPath)).href) {
253
+ create().catch((error) => {
254
+ console.error("Failed to create issue:", error instanceof Error ? error.message : error);
255
+ process.exitCode = 1;
256
+ });
257
+ }
@@ -3,6 +3,7 @@
3
3
  * Wraps the @linear/sdk to implement TaskSourceAdapter interface
4
4
  */
5
5
  import { LinearClient } from "@linear/sdk";
6
+ import { fetchIssuesPaged, MAX_FETCHED_ISSUES } from "../linear.js";
6
7
  export class LinearAdapter {
7
8
  type = "linear";
8
9
  client;
@@ -88,42 +89,26 @@ export class LinearAdapter {
88
89
  if (options.labelNames && options.labelNames.length > 0) {
89
90
  filter.labels = { name: { in: options.labelNames } };
90
91
  }
91
- if (options.assigneeEmail) {
92
- filter.assignee = { email: { eq: options.assigneeEmail } };
92
+ if (options.assigneeEmails && options.assigneeEmails.length > 0) {
93
+ filter.assignee = { email: { in: options.assigneeEmails } };
93
94
  }
94
- const issuesData = await this.client.issues({
95
- filter,
96
- first: options.limit ?? 50,
97
- });
95
+ const page = await fetchIssuesPaged(this.client, filter, "fetch issues", options.limit ?? MAX_FETCHED_ISSUES);
98
96
  const issues = [];
99
97
  const excludeLabels = new Set(options.excludeLabels ?? []);
100
- for (const issue of issuesData.nodes) {
101
- const state = await issue.state;
102
- const assignee = await issue.assignee;
103
- const labels = await issue.labels();
98
+ for (const issue of page.nodes) {
99
+ // Independent relations - awaiting them in sequence turns a list of
100
+ // 200 issues into 800 serial round-trips.
101
+ const [state, assignee, labels, parent] = await Promise.all([
102
+ issue.state,
103
+ issue.assignee,
104
+ issue.labels(),
105
+ issue.parent,
106
+ ]);
104
107
  const labelNames = labels.nodes.map((l) => l.name);
105
108
  // Skip if any label is in excluded list
106
109
  if (labelNames.some((name) => excludeLabels.has(name))) {
107
110
  continue;
108
111
  }
109
- const parent = await issue.parent;
110
- const attachmentsData = await issue.attachments();
111
- const commentsData = await issue.comments();
112
- const attachments = attachmentsData.nodes.map((a) => ({
113
- id: a.id,
114
- title: a.title,
115
- url: a.url,
116
- sourceType: a.sourceType ?? undefined,
117
- }));
118
- const comments = await Promise.all(commentsData.nodes.map(async (c) => {
119
- const user = await c.user;
120
- return {
121
- id: c.id,
122
- body: c.body,
123
- createdAt: c.createdAt.toISOString(),
124
- user: user?.displayName ?? user?.email,
125
- };
126
- }));
127
112
  issues.push({
128
113
  id: issue.identifier,
129
114
  sourceId: issue.id,
@@ -138,8 +123,8 @@ export class LinearAdapter {
138
123
  url: issue.url,
139
124
  parentIssueId: parent?.identifier,
140
125
  branchName: issue.branchName,
141
- attachments: attachments.length > 0 ? attachments : undefined,
142
- comments: comments.length > 0 ? comments : undefined,
126
+ attachments: undefined, // Loaded separately via getIssue()
127
+ comments: undefined, // Loaded separately via getIssue()
143
128
  });
144
129
  }
145
130
  return issues;
@@ -309,18 +294,25 @@ export class LinearAdapter {
309
294
  }
310
295
  async cancelIssue(sourceId) {
311
296
  try {
312
- // Find the "Cancelled" workflow state for this issue's team
297
+ // Resolve by statusType, not display name -- teams name this state
298
+ // "Canceled", "Cancelled", "Done - Cancelled", etc.
313
299
  const issue = await this.client.issue(sourceId);
314
300
  const team = await issue.team;
315
301
  if (!team) {
316
302
  return { success: false, error: "Could not determine issue team" };
317
303
  }
318
304
  const states = await this.client.workflowStates({
319
- filter: { team: { id: { eq: team.id } }, type: { eq: "cancelled" } },
305
+ filter: {
306
+ team: { id: { eq: team.id } },
307
+ type: { in: ["canceled", "cancelled"] },
308
+ },
320
309
  });
321
310
  const cancelledState = states.nodes[0];
322
311
  if (!cancelledState) {
323
- return { success: false, error: "No 'cancelled' workflow state found" };
312
+ return {
313
+ success: false,
314
+ error: `No cancelled-type workflow state found for team ${team.id}`,
315
+ };
324
316
  }
325
317
  const payload = await this.client.updateIssue(sourceId, {
326
318
  stateId: cancelledState.id,
@@ -27,7 +27,7 @@ export declare class TrelloAdapter implements TaskSourceAdapter {
27
27
  error?: string;
28
28
  }>;
29
29
  getInitData(teamId?: string): Promise<InitData>;
30
- createIssue(_options: CreateIssueOptions): Promise<{
30
+ createIssue(options: CreateIssueOptions): Promise<{
31
31
  success: boolean;
32
32
  issue?: SourceIssue;
33
33
  error?: string;
@@ -4,6 +4,12 @@
4
4
  */
5
5
  import { TrelloClient } from "../trello.js";
6
6
  import { detectPriorityFromLabels, } from "./types.js";
7
+ const PRIORITY_NAMES = {
8
+ 1: "urgent",
9
+ 2: "high",
10
+ 3: "medium",
11
+ 4: "low",
12
+ };
7
13
  export class TrelloAdapter {
8
14
  type = "trello";
9
15
  client;
@@ -103,6 +109,14 @@ export class TrelloAdapter {
103
109
  // Get first assignee
104
110
  const firstMemberId = card.idMembers[0];
105
111
  const assignee = firstMemberId ? memberMap.get(firstMemberId) : undefined;
112
+ // Filter by assignee if specified (OR logic)
113
+ if (options.assigneeEmails && options.assigneeEmails.length > 0) {
114
+ const email = assignee?.email?.toLowerCase();
115
+ if (!email ||
116
+ !options.assigneeEmails.some((e) => e.toLowerCase() === email)) {
117
+ continue;
118
+ }
119
+ }
106
120
  // Detect priority from labels
107
121
  const priority = detectPriorityFromLabels(labelNames);
108
122
  issues.push({
@@ -270,11 +284,85 @@ export class TrelloAdapter {
270
284
  currentCycle: undefined, // Trello has no cycle
271
285
  };
272
286
  }
273
- async createIssue(_options) {
274
- return {
275
- success: false,
276
- error: "createIssue is not yet supported for Trello",
277
- };
287
+ async createIssue(options) {
288
+ try {
289
+ const statuses = await this.getStatuses(options.teamId);
290
+ const targetStatus = options.statusId
291
+ ? statuses.find((status) => status.id === options.statusId)
292
+ : statuses[0];
293
+ if (!targetStatus) {
294
+ return {
295
+ success: false,
296
+ error: options.statusId
297
+ ? `Trello list ${options.statusId} is not open on board ${options.teamId}`
298
+ : `Trello board ${options.teamId} has no open lists`,
299
+ };
300
+ }
301
+ const labelIds = new Set(options.labelIds ?? []);
302
+ let knownLabels = [];
303
+ if (labelIds.size > 0 || (options.priority ?? 0) > 0) {
304
+ knownLabels = await this.getLabels(options.teamId);
305
+ }
306
+ if (options.priority && options.priority > 0) {
307
+ const priorityLabel = knownLabels.find((label) => detectPriorityFromLabels([label.name]) === options.priority);
308
+ if (!priorityLabel) {
309
+ return {
310
+ success: false,
311
+ error: `Trello board ${options.teamId} has no label matching priority "${PRIORITY_NAMES[options.priority]}"`,
312
+ };
313
+ }
314
+ labelIds.add(priorityLabel.id);
315
+ }
316
+ const card = await this.client.createCard({
317
+ name: options.title,
318
+ idList: targetStatus.id,
319
+ desc: options.description,
320
+ idMembers: options.assigneeId ? [options.assigneeId] : undefined,
321
+ idLabels: labelIds.size > 0 ? [...labelIds] : undefined,
322
+ });
323
+ if (!card.id || !card.shortLink || !card.name || !card.idList) {
324
+ return {
325
+ success: false,
326
+ error: "Trello created a card but returned an incomplete response",
327
+ };
328
+ }
329
+ const labelNames = Array.isArray(card.labels)
330
+ ? card.labels.map((label) => label.name).filter(Boolean)
331
+ : knownLabels
332
+ .filter((label) => labelIds.has(label.id))
333
+ .map((label) => label.name);
334
+ const status = statuses.find((candidate) => candidate.id === card.idList)?.name ??
335
+ targetStatus.name;
336
+ const assigneeId = Array.isArray(card.idMembers)
337
+ ? card.idMembers[0]
338
+ : options.assigneeId;
339
+ return {
340
+ success: true,
341
+ issue: {
342
+ id: card.shortLink,
343
+ sourceId: card.id,
344
+ title: card.name,
345
+ description: card.desc || undefined,
346
+ status,
347
+ statusId: card.idList,
348
+ assigneeId,
349
+ assigneeEmail: undefined,
350
+ priority: detectPriorityFromLabels(labelNames),
351
+ labels: labelNames,
352
+ url: card.url,
353
+ parentIssueId: undefined,
354
+ branchName: undefined,
355
+ attachments: undefined,
356
+ comments: undefined,
357
+ },
358
+ };
359
+ }
360
+ catch (error) {
361
+ return {
362
+ success: false,
363
+ error: error instanceof Error ? error.message : "Unknown error",
364
+ };
365
+ }
278
366
  }
279
367
  async updateIssue(_sourceId, _fields) {
280
368
  return {
@@ -95,7 +95,7 @@ export interface GetIssuesOptions {
95
95
  cycleId?: string;
96
96
  statusNames?: string[];
97
97
  labelNames?: string[];
98
- assigneeEmail?: string;
98
+ assigneeEmails?: string[];
99
99
  excludeLabels?: string[];
100
100
  limit?: number;
101
101
  }
@@ -1,4 +1,19 @@
1
+ import type { Issue, LinearClient } from "@linear/sdk";
1
2
  import { type Config, type StatusTransitions } from "../utils.js";
3
+ /** Issues requested per Linear API page. */
4
+ export declare const ISSUE_PAGE_SIZE = 100;
5
+ /** Hard stop so a broad filter can't pull an entire backlog. */
6
+ export declare const MAX_FETCHED_ISSUES = 500;
7
+ /**
8
+ * Fetch every issue matching `filter`, following pagination. Without this a
9
+ * single `first: n` request silently returns whichever page Linear hands back
10
+ * first, dropping the rest with no signal. `truncated` reports that `cap` cut
11
+ * the result short.
12
+ */
13
+ export declare function fetchIssuesPaged(client: LinearClient, filter: Record<string, unknown>, label: string, cap?: number): Promise<{
14
+ nodes: Issue[];
15
+ truncated: boolean;
16
+ }>;
2
17
  export interface WorkflowStateInfo {
3
18
  id: string;
4
19
  name: string;
@@ -1,5 +1,30 @@
1
- import { getLinearClient, getTeamId, } from "../utils.js";
1
+ import { getLinearClient, getTeamId, withRetry, } from "../utils.js";
2
2
  import { getFirstTodoStatus } from "./status-helpers.js";
3
+ /** Issues requested per Linear API page. */
4
+ export const ISSUE_PAGE_SIZE = 100;
5
+ /** Hard stop so a broad filter can't pull an entire backlog. */
6
+ export const MAX_FETCHED_ISSUES = 500;
7
+ /**
8
+ * Fetch every issue matching `filter`, following pagination. Without this a
9
+ * single `first: n` request silently returns whichever page Linear hands back
10
+ * first, dropping the rest with no signal. `truncated` reports that `cap` cut
11
+ * the result short.
12
+ */
13
+ export async function fetchIssuesPaged(client, filter, label, cap = MAX_FETCHED_ISSUES) {
14
+ const connection = await withRetry(() => client.issues({ filter, first: ISSUE_PAGE_SIZE }), { label });
15
+ while (connection.pageInfo.hasNextPage && connection.nodes.length < cap) {
16
+ const before = connection.nodes.length;
17
+ await withRetry(() => connection.fetchNext(), {
18
+ label: `${label} (next page)`,
19
+ });
20
+ if (connection.nodes.length === before)
21
+ break;
22
+ }
23
+ return {
24
+ nodes: connection.nodes.slice(0, cap),
25
+ truncated: connection.nodes.length > cap || connection.pageInfo.hasNextPage,
26
+ };
27
+ }
3
28
  export async function getWorkflowStates(config, teamKey) {
4
29
  const client = getLinearClient();
5
30
  const teamId = getTeamId(config, teamKey);
@@ -1,6 +1,8 @@
1
1
  import { createAdapter } from "./lib/adapters/index.js";
2
2
  import { displayTaskFull, getStatusIcon, PRIORITY_LABELS, } from "./lib/display.js";
3
- import { getSourceType, loadConfig, loadCycleData, loadLocalConfig, } from "./utils.js";
3
+ import { getStatusTransitions } from "./lib/linear.js";
4
+ import { getSyncStatuses } from "./lib/status-helpers.js";
5
+ import { getSourceType, getUserEmails, loadConfig, loadCycleData, loadLocalConfig, } from "./utils.js";
4
6
  function taskToMarkdown(task) {
5
7
  const lines = [];
6
8
  const priority = PRIORITY_LABELS[task.priority] || "None";
@@ -132,7 +134,68 @@ async function fetchIssueFromRemote(issueId) {
132
134
  })),
133
135
  };
134
136
  }
135
- async function searchIssuesFromRemote(filters) {
137
+ function resolveAssigneeScope(requested, userEmails) {
138
+ const emails = userEmails.map((e) => e.toLowerCase());
139
+ const value = requested?.toLowerCase();
140
+ if (!value || value === "me") {
141
+ return emails.length > 0 ? { kind: "user", emails } : { kind: "any" };
142
+ }
143
+ if (value === "unassigned") {
144
+ return { kind: "unassigned" };
145
+ }
146
+ return { kind: "match", needle: value };
147
+ }
148
+ /** True when the scope reaches outside what `ttt sync` stored locally. */
149
+ function scopeEscapesLocalCache(scope, userEmails) {
150
+ return userEmails.length > 0 && scope.kind !== "user";
151
+ }
152
+ function matchesAssignee(task, scope) {
153
+ switch (scope.kind) {
154
+ case "user":
155
+ return (!!task.assignee && scope.emails.includes(task.assignee.toLowerCase()));
156
+ case "unassigned":
157
+ return !task.assignee;
158
+ case "match":
159
+ return !!task.assignee?.toLowerCase().includes(scope.needle);
160
+ case "any":
161
+ return true;
162
+ }
163
+ }
164
+ /** Shared by both search paths so local and --remote filter identically. */
165
+ function applyFilters(tasks, filters, scope) {
166
+ return tasks.filter((task) => {
167
+ if (filters.label) {
168
+ const needle = filters.label.toLowerCase();
169
+ if (!task.labels.some((l) => l.toLowerCase().includes(needle))) {
170
+ return false;
171
+ }
172
+ }
173
+ if (filters.status) {
174
+ if (!task.status.toLowerCase().includes(filters.status.toLowerCase())) {
175
+ return false;
176
+ }
177
+ }
178
+ if (!matchesAssignee(task, scope)) {
179
+ return false;
180
+ }
181
+ if (filters.priority !== undefined && task.priority !== filters.priority) {
182
+ return false;
183
+ }
184
+ return true;
185
+ });
186
+ }
187
+ /**
188
+ * Expand a substring into the exact names the API filter needs. Names come
189
+ * from the source itself, not config, so a label added since `ttt init` is
190
+ * still found. Returns null when the substring matches nothing, meaning no
191
+ * issue can satisfy the filter.
192
+ */
193
+ function expandNames(needle, available) {
194
+ const lower = needle.toLowerCase();
195
+ const matches = available.filter((name) => name.toLowerCase().includes(lower));
196
+ return matches.length > 0 ? matches : null;
197
+ }
198
+ async function searchIssuesFromRemote(filters, scope) {
136
199
  const config = await loadConfig();
137
200
  const localConfig = await loadLocalConfig();
138
201
  const adapter = createAdapter(config);
@@ -142,104 +205,61 @@ async function searchIssuesFromRemote(filters) {
142
205
  console.error(`Team "${localConfig.team}" not found in config.`);
143
206
  return [];
144
207
  }
145
- // Use adapter to get issues with filters
146
- // Note: adapter API is simpler, filters applied in-memory for consistency
147
- const issues = await adapter.getIssues({
148
- teamId,
149
- statusNames: filters.status ? [filters.status] : undefined,
150
- labelNames: filters.label ? [filters.label] : undefined,
151
- limit: 50,
152
- });
153
- const tasks = [];
154
- for (const issue of issues) {
155
- // Apply additional filters
156
- if (filters.assignee) {
157
- const assigneeLower = filters.assignee.toLowerCase();
158
- if (assigneeLower === "me") {
159
- const userKeys = Array.isArray(localConfig.current_user)
160
- ? localConfig.current_user
161
- : localConfig.current_user
162
- ? [localConfig.current_user]
163
- : [];
164
- const userEmails = userKeys
165
- .map((key) => config.users[key]?.email?.toLowerCase())
166
- .filter((e) => !!e);
167
- if (userEmails.length === 0 ||
168
- !userEmails.includes(issue.assigneeEmail?.toLowerCase() ?? "")) {
169
- continue;
170
- }
171
- }
172
- else if (assigneeLower === "unassigned") {
173
- if (issue.assigneeEmail) {
174
- continue;
175
- }
176
- }
177
- else {
178
- if (!issue.assigneeEmail?.toLowerCase().includes(assigneeLower)) {
179
- continue;
180
- }
181
- }
182
- }
183
- if (filters.priority !== undefined && issue.priority !== filters.priority) {
184
- continue;
185
- }
186
- const task = {
187
- id: issue.id,
188
- linearId: issue.sourceId,
189
- sourceId: issue.sourceId,
190
- sourceType,
191
- title: issue.title,
192
- status: issue.status,
193
- localStatus: "pending",
194
- assignee: issue.assigneeEmail,
195
- priority: issue.priority,
196
- labels: issue.labels,
197
- description: issue.description,
198
- parentIssueId: issue.parentIssueId,
199
- url: issue.url,
200
- };
201
- tasks.push(task);
208
+ // Resolve substring filters to exact names so the query stays narrow while
209
+ // keeping the same matching semantics as the local path. Without --status
210
+ // the scope is what `ttt sync` stores, so an unfiltered --remote search
211
+ // answers the same question as the unfiltered local one.
212
+ let statusNames = getSyncStatuses(getStatusTransitions(config));
213
+ if (filters.status) {
214
+ const statuses = await adapter.getStatuses(teamId);
215
+ const expanded = expandNames(filters.status, statuses.map((s) => s.name));
216
+ if (!expanded)
217
+ return [];
218
+ statusNames = expanded;
202
219
  }
203
- return tasks;
204
- }
205
- async function searchIssuesFromLocal(data, filters) {
206
- let tasks = data.tasks;
220
+ let labelNames;
207
221
  if (filters.label) {
208
- const labelLower = filters.label.toLowerCase();
209
- tasks = tasks.filter((t) => t.labels.some((l) => l.toLowerCase().includes(labelLower)));
210
- }
211
- if (filters.status) {
212
- const statusLower = filters.status.toLowerCase();
213
- tasks = tasks.filter((t) => t.status.toLowerCase().includes(statusLower));
222
+ const labels = await adapter.getLabels(teamId);
223
+ const expanded = expandNames(filters.label, labels.map((l) => l.name));
224
+ if (!expanded)
225
+ return [];
226
+ labelNames = expanded;
214
227
  }
215
- if (filters.assignee) {
216
- const assigneeLower = filters.assignee.toLowerCase();
217
- if (assigneeLower === "unassigned") {
218
- tasks = tasks.filter((t) => !t.assignee);
219
- }
220
- else if (assigneeLower === "me") {
221
- const localConfig = await loadLocalConfig();
222
- const config = await loadConfig();
223
- const userKeys = Array.isArray(localConfig.current_user)
224
- ? localConfig.current_user
225
- : localConfig.current_user
226
- ? [localConfig.current_user]
227
- : [];
228
- const userEmails = userKeys
229
- .map((key) => config.users[key]?.email?.toLowerCase())
230
- .filter((e) => !!e);
231
- if (userEmails.length > 0) {
232
- tasks = tasks.filter((t) => t.assignee && userEmails.includes(t.assignee.toLowerCase()));
228
+ const issues = await adapter.getIssues({
229
+ teamId,
230
+ cycleId: config.current_cycle?.id,
231
+ statusNames,
232
+ labelNames,
233
+ assigneeEmails: scope.kind === "user" ? scope.emails : undefined,
234
+ });
235
+ const tasks = issues.map((issue) => ({
236
+ id: issue.id,
237
+ linearId: issue.sourceId,
238
+ sourceId: issue.sourceId,
239
+ sourceType,
240
+ title: issue.title,
241
+ status: issue.status,
242
+ localStatus: "pending",
243
+ assignee: issue.assigneeEmail,
244
+ priority: issue.priority,
245
+ labels: issue.labels,
246
+ description: issue.description,
247
+ parentIssueId: issue.parentIssueId,
248
+ url: issue.url,
249
+ }));
250
+ // Carry over local status so a remote search reads like the local one.
251
+ const data = await loadCycleData();
252
+ if (data) {
253
+ const localById = new Map(data.tasks.map((task) => [task.id, task]));
254
+ for (const task of tasks) {
255
+ const local = localById.get(task.id);
256
+ if (local) {
257
+ task.localStatus = local.localStatus;
258
+ task.estimate = local.estimate;
233
259
  }
234
260
  }
235
- else {
236
- tasks = tasks.filter((t) => t.assignee?.toLowerCase().includes(assigneeLower));
237
- }
238
261
  }
239
- if (filters.priority !== undefined) {
240
- tasks = tasks.filter((t) => t.priority === filters.priority);
241
- }
242
- return tasks;
262
+ return applyFilters(tasks, filters, scope);
243
263
  }
244
264
  async function show() {
245
265
  const args = process.argv.slice(2);
@@ -259,6 +279,11 @@ Options:
259
279
  --user <email> Filter by assignee (use "me" for yourself, "unassigned" for no assignee)
260
280
  --priority <n> Filter by priority (0=None, 1=Urgent, 2=High, 3=Medium, 4=Low)
261
281
 
282
+ Searches are scoped the same way ttt sync is: current cycle, current_user,
283
+ and Todo/In Progress. --status and --user replace the corresponding scope.
284
+ The local cache only holds current_user's issues, so searching another
285
+ assignee needs --remote.
286
+
262
287
  Examples:
263
288
  ttt show # Show all issues in local cycle data
264
289
  ttt show MP-624 # Show specific issue from local data
@@ -267,13 +292,17 @@ Examples:
267
292
  ttt show --label frontend # Filter local issues by label
268
293
  ttt show --status "In Progress" --user me # My in-progress issues
269
294
  ttt show --priority 1 # Show all urgent issues
295
+ ttt show --user tony --remote # Someone else's issues (needs --remote)
270
296
  ttt show --export # Export all issues as markdown`);
271
297
  process.exit(0);
272
298
  }
273
299
  const useRemote = args.includes("--remote");
274
300
  const exportMarkdown = args.includes("--export");
275
- // Parse filters
301
+ // Parse filters. Option values are recorded so the issue-ID scan below can
302
+ // skip them - a value like "unassigned" otherwise matches the Trello
303
+ // shortLink pattern and hijacks the whole command.
276
304
  const filters = {};
305
+ const optionValues = new Set();
277
306
  for (let i = 0; i < args.length; i++) {
278
307
  const arg = args[i];
279
308
  if (arg === "--label" && args[i + 1]) {
@@ -288,36 +317,27 @@ Examples:
288
317
  else if (arg === "--priority" && args[i + 1]) {
289
318
  filters.priority = parseInt(args[++i], 10);
290
319
  }
291
- }
292
- // Check if this is a search (has filters) or single issue lookup
293
- const hasFilters = Object.keys(filters).length > 0;
294
- // Find issue ID: Linear-style (MP-123) or Trello shortLink (8+ alphanumeric)
295
- const issueId = args.find((arg) => !arg.startsWith("-") &&
296
- (arg.match(/^[A-Z]+-\d+$/i) || arg.match(/^[A-Za-z0-9]{8,}$/)));
297
- // If no issue ID and no filters, show all local issues
298
- if (!issueId && !hasFilters) {
299
- const data = await loadCycleData();
300
- if (!data) {
301
- console.error("No cycle data found. Run ttt sync first.");
302
- process.exit(1);
303
- }
304
- if (exportMarkdown) {
305
- console.log(tasksToMarkdownList(data.tasks));
306
- }
307
320
  else {
308
- displayTaskList(data.tasks);
321
+ continue;
309
322
  }
310
- return;
323
+ optionValues.add(i);
311
324
  }
312
- // Search mode: has filters but no specific issue ID
313
- if (hasFilters && !issueId) {
325
+ // Find issue ID: Linear-style (MP-123) or Trello shortLink (8+ alphanumeric)
326
+ const issueId = args.find((arg, i) => !optionValues.has(i) &&
327
+ !arg.startsWith("-") &&
328
+ (arg.match(/^[A-Z]+-\d+$/i) || arg.match(/^[A-Za-z0-9]{8,}$/)));
329
+ // List mode: no specific issue ID. Filters are optional; --remote runs the
330
+ // same query live instead of reading the cache.
331
+ if (!issueId) {
332
+ const userEmails = await getUserEmails();
333
+ const scope = resolveAssigneeScope(filters.assignee, userEmails);
314
334
  let tasks;
315
335
  if (useRemote) {
316
336
  const config = await loadConfig();
317
337
  const sourceType = getSourceType(config);
318
338
  const sourceName = sourceType === "trello" ? "Trello" : "Linear";
319
339
  console.error(`Searching issues from ${sourceName}...`);
320
- tasks = await searchIssuesFromRemote(filters);
340
+ tasks = await searchIssuesFromRemote(filters, scope);
321
341
  }
322
342
  else {
323
343
  const data = await loadCycleData();
@@ -325,7 +345,14 @@ Examples:
325
345
  console.error("No cycle data found. Run ttt sync first.");
326
346
  process.exit(1);
327
347
  }
328
- tasks = await searchIssuesFromLocal(data, filters);
348
+ tasks = applyFilters(data.tasks, filters, scope);
349
+ // The cache only holds what `ttt sync` scoped to current_user, so an
350
+ // empty result here is a scope limit, not an answer.
351
+ if (tasks.length === 0 && scopeEscapesLocalCache(scope, userEmails)) {
352
+ console.error(`Local cache only contains issues assigned to ${userEmails.join(", ")}.`);
353
+ console.error(`Add --remote to search others: ttt show --user ${filters.assignee} --remote`);
354
+ return;
355
+ }
329
356
  }
330
357
  if (exportMarkdown) {
331
358
  console.log(tasksToMarkdownList(tasks));
@@ -1,7 +1,8 @@
1
1
  import { createAdapter } from "./lib/adapters/index.js";
2
2
  import { clearAllOutput, clearIssueImages, downloadLinearImage, downloadTrelloFile, ensureOutputDir, extractImageUrls, isLinearImageUrl, } from "./lib/files.js";
3
+ import { fetchIssuesPaged, MAX_FETCHED_ISSUES } from "./lib/linear.js";
3
4
  import { getReviewStatuses, getSyncStatuses, resolveLocalStatus, } from "./lib/status-helpers.js";
4
- import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, preserveLocalTaskFields, saveConfig, saveCycleData, withRetry, } from "./utils.js";
5
+ import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, getUserEmails, loadConfig, loadCycleData, loadLocalConfig, preserveLocalTaskFields, saveConfig, saveCycleData, withRetry, } from "./utils.js";
5
6
  async function downloadEmbeddedImages(texts, issueId, attachments, outputDir, downloadFile, titlePrefix, sourceType, filterUrl) {
6
7
  let imageIndex = 0;
7
8
  for (const text of texts) {
@@ -45,6 +46,7 @@ Options:
45
46
  What it does:
46
47
  - Fetches active cycle from Linear
47
48
  - Downloads issues with Todo/In Progress status (or all with --all)
49
+ - Filters by current_user assignee (all users if none configured)
48
50
  - Filters by label if configured
49
51
  - Preserves local status for existing tasks
50
52
  - Updates config with new cycle info
@@ -201,9 +203,11 @@ Examples:
201
203
  // Phase 4: Fetch current issues with full content
202
204
  const filterLabels = localConfig.labels;
203
205
  const syncStatuses = getSyncStatuses(statusTransitions);
206
+ const userEmails = await getUserEmails();
204
207
  const labelDesc = filterLabels && filterLabels.length > 0
205
208
  ? ` with labels: ${filterLabels.join(", ")}`
206
209
  : "";
210
+ const assigneeDesc = userEmails.length > 0 ? ` assigned to ${userEmails.join(", ")}` : "";
207
211
  let issues;
208
212
  if (singleIssueId) {
209
213
  // Sync single issue by ID
@@ -221,9 +225,10 @@ Examples:
221
225
  const statusDesc = syncAll
222
226
  ? "all statuses"
223
227
  : `${syncStatuses.join("/")} status`;
224
- console.log(`Fetching issues (${statusDesc})${labelDesc}...`);
225
- // Build filter - label is optional; cycle is skipped if team has no
226
- // active cycle (Linear lets teams disable cycles entirely).
228
+ console.log(`Fetching issues (${statusDesc})${assigneeDesc}${labelDesc}...`);
229
+ // Build filter - label and assignee are optional; cycle is skipped if
230
+ // the team has no active cycle (Linear lets teams disable cycles
231
+ // entirely). No configured user means team-wide sync.
227
232
  const issueFilter = {
228
233
  team: { id: { eq: teamId } },
229
234
  };
@@ -236,10 +241,15 @@ Examples:
236
241
  if (filterLabels && filterLabels.length > 0) {
237
242
  issueFilter.labels = { name: { in: filterLabels } };
238
243
  }
239
- issues = await withRetry(() => client.issues({
240
- filter: issueFilter,
241
- first: 50,
242
- }), { label: "fetch issues" });
244
+ if (userEmails.length > 0) {
245
+ issueFilter.assignee = { email: { in: userEmails } };
246
+ }
247
+ // Paginate so a large cycle isn't silently truncated to one page.
248
+ const page = await fetchIssuesPaged(client, issueFilter, "fetch issues");
249
+ if (page.truncated) {
250
+ console.warn(`\nWarning: stopped at ${MAX_FETCHED_ISSUES} issues; more matched the filter.`);
251
+ }
252
+ issues = { nodes: page.nodes };
243
253
  }
244
254
  if (issues.nodes.length === 0) {
245
255
  console.log(`No issues found in current cycle${labelDesc}.`);
@@ -492,10 +502,12 @@ async function syncTrello(config, localConfig, options) {
492
502
  ? "all statuses"
493
503
  : `${syncStatuses.join("/")} status`;
494
504
  const filterLabels = localConfig.labels;
505
+ const userEmails = await getUserEmails();
495
506
  const labelDesc = filterLabels && filterLabels.length > 0
496
507
  ? ` with labels: ${filterLabels.join(", ")}`
497
508
  : "";
498
- console.log(`Fetching cards (${statusDesc})${labelDesc}...`);
509
+ const assigneeDesc = userEmails.length > 0 ? ` assigned to ${userEmails.join(", ")}` : "";
510
+ console.log(`Fetching cards (${statusDesc})${assigneeDesc}${labelDesc}...`);
499
511
  let issues;
500
512
  if (singleIssueId) {
501
513
  // Sync single issue
@@ -508,8 +520,9 @@ async function syncTrello(config, localConfig, options) {
508
520
  teamId,
509
521
  statusNames: syncAll ? undefined : syncStatuses,
510
522
  labelNames: filterLabels,
523
+ assigneeEmails: userEmails.length > 0 ? userEmails : undefined,
511
524
  excludeLabels: localConfig.exclude_labels,
512
- limit: 100,
525
+ limit: MAX_FETCHED_ISSUES,
513
526
  });
514
527
  }
515
528
  if (issues.length === 0) {
@@ -48,7 +48,7 @@ Examples:
48
48
  if (currentUserEmails.length === 0)
49
49
  return true; // No filter = all users
50
50
  if (!t.assignee)
51
- return true; // Include unassigned tasks
51
+ return false;
52
52
  return currentUserEmails.includes(t.assignee.toLowerCase());
53
53
  })
54
54
  .sort((a, b) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.7.7",
3
+ "version": "3.9.3",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "files": [
15
15
  "dist",
16
16
  ".claude-plugin",
17
+ ".codex-plugin",
17
18
  "commands",
18
19
  "skills"
19
20
  ],
@@ -3,12 +3,13 @@ name: managing-linear-tasks
3
3
  description: Use when the user mentions Linear or Trello issues, references issue IDs like MP-123, or asks to sync, show, start, complete, create, assign, edit, cancel, comment on, or check status of tasks.
4
4
  ---
5
5
 
6
- <law>
7
- ALL task operations MUST go through the `ttt` CLI via Bash tool.
8
- NEVER manually edit `.ttt/` files (cycle.toon, config.toon, local.toon).
9
- NEVER fabricate issue data — always run the CLI to get real data.
10
- After completing any task, MUST execute `/ttt:done -m "summary"`.
11
- </law>
6
+ ## 執行方式
7
+
8
+ Codex Claude Code 中,使用可用的 shell 工具執行 `ttt` CLI。
9
+ 任務資料以 CLI 的查詢結果為準;透過 CLI 更新 `.ttt/` 內的設定與狀態。
10
+ 使用者要求完成任務時,執行 `ttt done -m "摘要"`。
11
+ Claude Code 也提供 `/ttt:*` slash commands,對應下方相同的 CLI 操作。
12
+
12
13
 
13
14
  # Task Manager (Linear/Trello)
14
15
 
@@ -16,23 +17,23 @@ Manage developer task workflows using the `ttt` CLI.
16
17
 
17
18
  ## Command Router
18
19
 
19
- Match user intent to the correct `/ttt:*` command:
20
+ 依使用者意圖選擇下列 `ttt` CLI 指令:
20
21
 
21
22
  | User Intent | Command | Example |
22
23
  |-------------|---------|---------|
23
- | Sync/fetch issues | `/ttt:sync` | "sync my issues", "pull from Linear" |
24
- | Show/search issues | `/ttt:show` | "show MP-624", "list my tasks", "what issues do I have" |
25
- | Start working on a task | `/ttt:work-on` | "work on next", "start MP-624" |
26
- | Create a new issue | `/ttt:create` | "create issue", "open a ticket", "new task" |
27
- | Reassign an issue | `/ttt:assign` | "assign MP-624 to john", "reassign to jane" |
28
- | Edit issue fields | `/ttt:edit` | "rename MP-624", "change priority", "update labels" |
29
- | Cancel an issue | `/ttt:cancel` | "cancel MP-624", "abandon this task" |
30
- | Record an estimate | `/ttt:estimate` | "estimate MP-624 as 6h", "save 2.5h estimate" |
31
- | Check/change status | `/ttt:status` | "what's my current task", "set MP-624 to blocked" |
32
- | Add a comment | `/ttt:comment` | "comment on MP-624", "add note to task" |
33
- | Complete a task | `/ttt:done` | "done", "mark complete", "finish task" |
34
-
35
- **When a matching intent is detected, invoke the corresponding `/ttt:*` slash command.**
24
+ | Sync/fetch issues | `ttt sync` | "sync my issues", "pull from Linear" |
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" |
27
+ | Create a new issue | `ttt create` | "create issue", "open a ticket", "new task" |
28
+ | Reassign an issue | `ttt assign` | "assign MP-624 to john", "reassign to jane" |
29
+ | Edit issue fields | `ttt edit` | "rename MP-624", "change priority", "update labels" |
30
+ | Cancel an issue | `ttt cancel` | "cancel MP-624", "abandon this task" |
31
+ | Record an estimate | `ttt estimate` | "estimate MP-624 as 6h", "save 2.5h estimate" |
32
+ | Check/change status | `ttt status` | "what's my current task", "set MP-624 to blocked" |
33
+ | Add a comment | `ttt comment` | "comment on MP-624", "add note to task" |
34
+ | Complete a task | `ttt done` | "done", "mark complete", "finish task" |
35
+
36
+ **意圖符合時,透過 shell 工具執行對應的 `ttt` CLI 指令。**
36
37
 
37
38
  ## Quick Reference
38
39
 
@@ -96,7 +97,7 @@ ttt sync → ttt work-on next → ttt estimate <id> <hours> → [implement] →
96
97
  | "I'll edit `cycle.toon` directly, faster" | `cycle.toon` is auto-generated. Manual edits get overwritten on next `ttt sync`. Always go through the CLI. |
97
98
  | "I remember this issue ID, no need to sync" | Remote status, assignee, priority may have changed. Run `ttt sync` or `ttt show <id> --remote` first. |
98
99
  | "I'll create the issue in the Linear/Trello UI instead" | Bypassing `ttt create` leaves the new issue outside local cycle data. Use the CLI so it gets tracked. |
99
- | "Task is obvious, skip `/ttt:done`" | `/ttt:done` syncs local + remote status, posts a completion comment, reads git commit info. Skipping leaves state inconsistent. |
100
+ | "Task is obvious, skip `ttt done`" | `ttt done` syncs local + remote status, posts a completion comment, reads git commit info. Skipping leaves state inconsistent. |
100
101
  | "I know what the task needs without reading it" | Fabricated assumptions produce wrong work. Run `ttt show <id>` before starting. |
101
102
  | "Fetch issue details via Linear MCP / web UI" | The CLI is authoritative and token-efficient. Use `ttt show` / `ttt sync`, not alternate data paths. |
102
103
 
@@ -109,17 +110,17 @@ digraph ttt_router {
109
110
  user [label="User mentions\nLinear/Trello", shape=doublecircle];
110
111
  classify [label="Classify intent", shape=diamond];
111
112
 
112
- sync [label="/ttt:sync", shape=box];
113
- show [label="/ttt:show", shape=box];
114
- work_on [label="/ttt:work-on", shape=box];
115
- create [label="/ttt:create", shape=box];
116
- assign [label="/ttt:assign", shape=box];
117
- edit [label="/ttt:edit", shape=box];
118
- cancel [label="/ttt:cancel", shape=box];
119
- estimate [label="/ttt:estimate", shape=box];
120
- status [label="/ttt:status", shape=box];
121
- comment [label="/ttt:comment", shape=box];
122
- done [label="/ttt:done", shape=box];
113
+ sync [label="ttt sync", shape=box];
114
+ show [label="ttt show", shape=box];
115
+ work_on [label="ttt work-on", shape=box];
116
+ create [label="ttt create", shape=box];
117
+ assign [label="ttt assign", shape=box];
118
+ edit [label="ttt edit", shape=box];
119
+ cancel [label="ttt cancel", shape=box];
120
+ estimate [label="ttt estimate", shape=box];
121
+ status [label="ttt status", shape=box];
122
+ comment [label="ttt comment", shape=box];
123
+ done [label="ttt done", shape=box];
123
124
 
124
125
  user -> classify;
125
126
  classify -> sync [label="fetch / pull"];