team-toon-tack 3.7.3 → 3.7.5

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.
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Task management tools for Claude Code - supports Linear and Trello, efficient workflow without MCP overhead",
9
- "version": "1.1.0"
9
+ "version": "1.2.1"
10
10
  },
11
11
  "plugins": [
12
12
  {
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.7.0"
16
+ "version": "2.8.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.7.0",
4
+ "version": "2.8.1",
5
5
  "author": {
6
6
  "name": "wayne930242",
7
7
  "email": "wayne930242@gmail.com"
@@ -48,30 +48,68 @@ Options:
48
48
  --dry-run Pick task without changing status (preview only)
49
49
  ```
50
50
 
51
- ## After Execution
51
+ ## Workflow — Plan → Test → Code → Review
52
52
 
53
- 1. Read the task description and requirements from the output
54
- 2. Check out the suggested branch: `git checkout -b <branch-name>`
55
- 3. Check for project-specific **work-on skill** in `.claude/skills/`
56
- 4. Begin implementation
53
+ Follow this loop for every task.
54
+ Skipping phases is not "pragmatic", it is debt.
57
55
 
58
- ## CRITICAL: Task Completion Contract
56
+ ### 1. Branch
59
57
 
60
- When the task is completed, you MUST execute:
61
-
62
- ```
63
- /ttt:done -m "completion summary"
58
+ ```bash
59
+ git checkout -b <suggested-branch-name>
64
60
  ```
65
61
 
66
- A task is NOT complete until `/ttt:done` is executed. Do NOT skip this step.
62
+ ### 2. Plan
63
+
64
+ Scope decides depth.
65
+
66
+ - Unclear requirements or 3+ files touched → invoke `superpowers:brainstorming`, then `superpowers:writing-plans`.
67
+ - Clear and small (≤2 files, obvious change) → state a 2–3 bullet plan inline before coding.
68
+
69
+ Never go straight to code.
70
+ Planning collapses ambiguity before implementation.
71
+
72
+ ### 3. Test First (TDD)
73
+
74
+ Invoke `superpowers:test-driven-development`.
75
+
76
+ Red → Green → Refactor per behavior:
77
+
78
+ 1. Write one failing test naming the behavior.
79
+ 2. Run test — verify it fails for the expected reason.
80
+ 3. Write minimal code to pass.
81
+ 4. Run test — verify pass, other tests still green.
82
+ 5. Refactor if needed, keep green.
83
+
84
+ No production code without a failing test first.
85
+
86
+ ### 4. Review (before `/ttt:done`)
87
+
88
+ Invoke `superpowers:verification-before-completion`.
89
+
90
+ Run and confirm output:
91
+
92
+ - Full test suite passes.
93
+ - `npm run lint` / `npm run type` clean (or project-specific commands).
94
+ - Only requested scope changed — no drive-by edits.
95
+ - No debug logs, stubs, TODOs.
96
+
97
+ Evidence before assertions.
98
+ Do not claim complete without running the commands.
99
+
100
+ ### 5. Complete
101
+
102
+ ```text
103
+ /ttt:done -m "summary"
104
+ ```
67
105
 
68
- ## Work-On Skill Integration
106
+ A task is NOT complete until `/ttt:done` runs.
107
+ This is MANDATORY.
69
108
 
70
- After starting a task, check for project-specific work guidelines:
109
+ ## Project-Specific Skill
71
110
 
72
- 1. Look for `work-on` or `start-work` skills in `.claude/skills/`
73
- 2. If found, follow those guidelines
74
- 3. If not found, suggest creating one with `/ttt:write-work-on-skill`
111
+ Check `.claude/skills/work-on/` or `.claude/skills/start-work/` for project-specific lint / type / test commands and conventions.
112
+ If missing, suggest `/ttt:write-work-on-skill` — it scaffolds a skill following this same 4-phase structure.
75
113
 
76
114
  ## Error Handling
77
115
 
@@ -44,68 +44,71 @@ Look for: `lint`, `type-check`, `test`, `check`, `validate`, `format`
44
44
 
45
45
  ### 4. Create Skill File
46
46
 
47
- Create `.claude/skills/{{ $1 | default: "work-on" }}/SKILL.md`:
47
+ Create `.claude/skills/{{ $1 | default: "work-on" }}/SKILL.md` using the Plan → Test → Code → Review structure:
48
48
 
49
49
  ```markdown
50
50
  ---
51
51
  name: {{ $1 | default: "work-on" }}
52
- description: Project-specific best practices for starting and completing tasks
52
+ description: Project workflow Plan Test Code → Review. Invoke after ttt work-on picks a task.
53
53
  ---
54
54
 
55
- # Work-On Skill
55
+ # Work-On Skill ({{ project-name }})
56
56
 
57
- Best practices for working on tasks in this project.
57
+ Follow Plan Test Code Review for every task.
58
+ Skipping phases is debt, not pragmatism.
58
59
 
59
- ## Before Starting
60
+ ## 1. Plan
60
61
 
61
- ### 1. Validation Checks
62
- \`\`\`bash
63
- {{ lint-command }}
64
- {{ type-check-command }}
65
- \`\`\`
62
+ - Unclear scope or 3+ files → use `superpowers:brainstorming` → `superpowers:writing-plans`.
63
+ - Small, clear change → state a 2–3 bullet plan inline before coding.
66
64
 
67
- ### 2. Branch Naming
68
- - Feature: `feature/<issue-id>-<short-description>`
69
- - Bugfix: `fix/<issue-id>-<short-description>`
70
- - Refactor: `refactor/<issue-id>-<short-description>`
65
+ Branch naming:
66
+ - `feature/<issue-id>-<slug>`
67
+ - `fix/<issue-id>-<slug>`
68
+ - `refactor/<issue-id>-<slug>`
71
69
 
72
- ### 3. Environment Setup
73
- - Ensure dependencies are installed
74
- - Check for required environment variables
70
+ ## 2. Test First (TDD)
75
71
 
76
- ## Code Style
72
+ Invoke `superpowers:test-driven-development`.
73
+ No production code without a failing test first.
77
74
 
78
- ### Formatting
79
- {{ formatting-rules }}
75
+ Red → Green → Refactor:
76
+ \`\`\`bash
77
+ {{ test-command }}
78
+ \`\`\`
80
79
 
81
- ### Naming Conventions
82
- {{ naming-conventions }}
80
+ - One behavior per test.
81
+ - Real code, mocks only when unavoidable.
82
+ - Watch each test fail for the expected reason before writing code.
83
83
 
84
- ### File Structure
85
- {{ file-structure-notes }}
84
+ ## 3. Code
86
85
 
87
- ## Before Completing
86
+ Minimal code to pass the current test.
87
+ No features beyond the test's scope.
88
+ Match existing style — do not refactor adjacent code.
88
89
 
89
- ### 1. Run All Checks
90
- \`\`\`bash
91
- {{ combined-validation-command }}
92
- \`\`\`
90
+ ## 4. Review (before `/ttt:done`)
93
91
 
94
- ### 2. Commit Message Format
95
- \`\`\`
96
- <type>(<scope>): <description>
92
+ Invoke `superpowers:verification-before-completion`.
93
+ Run every command and confirm the actual output.
97
94
 
98
- <body>
95
+ \`\`\`bash
96
+ {{ lint-command }}
97
+ {{ type-check-command }}
98
+ {{ test-command }}
99
99
  \`\`\`
100
100
 
101
- Types: feat, fix, refactor, docs, test, chore
101
+ Checklist:
102
+ - [ ] All tests pass (new tests went red → green).
103
+ - [ ] No lint / type errors.
104
+ - [ ] Changes scoped to this task only.
105
+ - [ ] No debug logs, stubs, TODOs, commented-out code.
106
+ - [ ] Commit message: `<type>(<scope>): <description>` — types: feat, fix, refactor, docs, test, chore.
107
+
108
+ ## 5. Complete
102
109
 
103
- ### 3. Self-Review Checklist
104
- - [ ] Code compiles without errors
105
- - [ ] All tests pass
106
- - [ ] No linting errors
107
- - [ ] Changes are focused on the task
108
- - [ ] No debug code or console.logs left
110
+ \`/ttt:done -m "summary"\` — MANDATORY.
111
+ A task is NOT complete until `/ttt:done` runs.
109
112
  ```
110
113
 
111
114
  ### 5. Output
@@ -134,6 +134,12 @@ export class TrelloAdapter {
134
134
  const card = await this.client.getCard(issueId);
135
135
  if (!card)
136
136
  return null;
137
+ if (!Array.isArray(card.labels)) {
138
+ card.labels = [];
139
+ }
140
+ if (!Array.isArray(card.idMembers)) {
141
+ card.idMembers = [];
142
+ }
137
143
  // Get lists for status name lookup
138
144
  if (!this.listsCache.has(card.idBoard)) {
139
145
  await this.getStatuses(card.idBoard);
@@ -185,7 +191,8 @@ export class TrelloAdapter {
185
191
  comments: sourceComments.length > 0 ? sourceComments : undefined,
186
192
  };
187
193
  }
188
- catch {
194
+ catch (error) {
195
+ console.warn(`[trello] getIssue(${issueId}) failed:`, error instanceof Error ? error.message : error);
189
196
  return null;
190
197
  }
191
198
  }
@@ -98,7 +98,7 @@ export class TrelloClient {
98
98
  * Get a specific card by ID
99
99
  */
100
100
  async getCard(cardId) {
101
- return this.request("GET", `/cards/${cardId}?fields=id,name,desc,url,shortUrl,shortLink,closed,pos,due,dueComplete,idBoard,idList,idMembers,idLabels,badges,dateLastActivity&attachments=false&members=false&labels=true`);
101
+ return this.request("GET", `/cards/${cardId}?fields=id,name,desc,url,shortUrl,shortLink,closed,pos,due,dueComplete,idBoard,idList,idMembers,idLabels,labels,badges,dateLastActivity&attachments=false&members=false`);
102
102
  }
103
103
  /**
104
104
  * Get card attachments
@@ -291,8 +291,9 @@ Examples:
291
291
  }
292
292
  // Check if this is a search (has filters) or single issue lookup
293
293
  const hasFilters = Object.keys(filters).length > 0;
294
- // Find issue ID (argument that doesn't start with -)
295
- const issueId = args.find((arg) => !arg.startsWith("-") && arg.match(/^[A-Z]+-\d+$/i));
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,}$/)));
296
297
  // If no issue ID and no filters, show all local issues
297
298
  if (!issueId && !hasFilters) {
298
299
  const data = await loadCycleData();
@@ -1,7 +1,7 @@
1
1
  import { createAdapter } from "./lib/adapters/index.js";
2
2
  import { clearAllOutput, clearIssueImages, downloadLinearImage, downloadTrelloFile, ensureOutputDir, extractImageUrls, isLinearImageUrl, } from "./lib/files.js";
3
3
  import { getReviewStatuses, getSyncStatuses, resolveLocalStatus, } from "./lib/status-helpers.js";
4
- import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, preserveLocalTaskFields, saveConfig, saveCycleData, } from "./utils.js";
4
+ import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, preserveLocalTaskFields, saveConfig, saveCycleData, withRetry, } from "./utils.js";
5
5
  async function downloadEmbeddedImages(texts, issueId, attachments, outputDir, downloadFile, titlePrefix, sourceType, filterUrl) {
6
6
  let imageIndex = 0;
7
7
  for (const text of texts) {
@@ -88,8 +88,10 @@ Examples:
88
88
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
89
89
  // Phase 1: Fetch active cycle directly from team (if the team uses cycles)
90
90
  console.log("Fetching latest cycle...");
91
- const team = await client.team(teamId);
92
- const activeCycle = await team.activeCycle;
91
+ const team = await withRetry(() => client.team(teamId), {
92
+ label: "fetch team",
93
+ });
94
+ const activeCycle = await withRetry(async () => team.activeCycle ?? undefined, { label: "fetch active cycle" });
93
95
  let cycleId;
94
96
  let cycleName;
95
97
  const existingData = await loadCycleData();
@@ -131,9 +133,9 @@ Examples:
131
133
  console.log("No active cycle on this team — syncing without cycle filter.");
132
134
  }
133
135
  // Phase 2: Fetch workflow states and get status mappings
134
- const workflowStates = await client.workflowStates({
136
+ const workflowStates = await withRetry(() => client.workflowStates({
135
137
  filter: { team: { id: { eq: teamId } } },
136
- });
138
+ }), { label: "fetch workflow states" });
137
139
  const stateMap = new Map(workflowStates.nodes.map((s) => [s.name, s.id]));
138
140
  // Get status names from config or use defaults
139
141
  const statusTransitions = config.status_transitions || {
@@ -170,9 +172,9 @@ Examples:
170
172
  }
171
173
  if (targetStateId) {
172
174
  try {
173
- const payload = await client.updateIssue(task.linearId, {
175
+ const payload = await withRetry(() => client.updateIssue(task.linearId, {
174
176
  stateId: targetStateId,
175
- });
177
+ }), { label: `update ${task.id}` });
176
178
  if (!payload.success) {
177
179
  throw new Error("Linear mutation returned success=false");
178
180
  }
@@ -206,7 +208,7 @@ Examples:
206
208
  if (singleIssueId) {
207
209
  // Sync single issue by ID
208
210
  console.log(`Fetching issue ${singleIssueId}...`);
209
- const searchResult = await client.searchIssues(singleIssueId);
211
+ const searchResult = await withRetry(() => client.searchIssues(singleIssueId), { label: `search ${singleIssueId}` });
210
212
  const matchingIssue = searchResult.nodes.find((i) => i.identifier === singleIssueId);
211
213
  if (!matchingIssue) {
212
214
  console.error(`Issue ${singleIssueId} not found.`);
@@ -234,10 +236,10 @@ Examples:
234
236
  if (filterLabels && filterLabels.length > 0) {
235
237
  issueFilter.labels = { name: { in: filterLabels } };
236
238
  }
237
- issues = await client.issues({
239
+ issues = await withRetry(() => client.issues({
238
240
  filter: issueFilter,
239
241
  first: 50,
240
- });
242
+ }), { label: "fetch issues" });
241
243
  }
242
244
  if (issues.nodes.length === 0) {
243
245
  console.log(`No issues found in current cycle${labelDesc}.`);
@@ -253,19 +255,34 @@ Examples:
253
255
  processedCount++;
254
256
  process.stdout.write(`\r Processing ${processedCount}/${totalIssues}...`);
255
257
  // Fetch full issue to get all relations (searchIssues returns IssueSearchResult which lacks some methods)
256
- const issue = await client.issue(issueNode.id);
257
- const assignee = await issue.assignee;
258
+ const issueLabel = issueNode.identifier;
259
+ const issue = await withRetry(() => client.issue(issueNode.id), {
260
+ label: `fetch ${issueLabel}`,
261
+ });
262
+ const assignee = await withRetry(() => Promise.resolve(issue.assignee), {
263
+ label: `fetch ${issueLabel} assignee`,
264
+ });
258
265
  const assigneeEmail = assignee?.email;
259
- const labels = await issue.labels();
266
+ const labels = await withRetry(() => issue.labels(), {
267
+ label: `fetch ${issueLabel} labels`,
268
+ });
260
269
  const labelNames = labels.nodes.map((l) => l.name);
261
270
  // Skip if any label is in excluded list
262
271
  if (labelNames.some((name) => excludedLabels.has(name))) {
263
272
  continue;
264
273
  }
265
- const state = await issue.state;
266
- const parent = await issue.parent;
267
- const attachmentsData = await issue.attachments();
268
- const commentsData = await issue.comments();
274
+ const state = await withRetry(() => Promise.resolve(issue.state), {
275
+ label: `fetch ${issueLabel} state`,
276
+ });
277
+ const parent = await withRetry(() => Promise.resolve(issue.parent), {
278
+ label: `fetch ${issueLabel} parent`,
279
+ });
280
+ const attachmentsData = await withRetry(() => issue.attachments(), {
281
+ label: `fetch ${issueLabel} attachments`,
282
+ });
283
+ const commentsData = await withRetry(() => issue.comments(), {
284
+ label: `fetch ${issueLabel} comments`,
285
+ });
269
286
  // Clear old images for this issue before downloading new ones
270
287
  await clearIssueImages(outputPath, issue.identifier);
271
288
  // Build attachments list and download Linear images
@@ -288,7 +305,9 @@ Examples:
288
305
  }
289
306
  // Build comments list
290
307
  const comments = await Promise.all(commentsData.nodes.map(async (c) => {
291
- const user = await c.user;
308
+ const user = await withRetry(() => Promise.resolve(c.user), {
309
+ label: `fetch ${issueLabel} comment user`,
310
+ });
292
311
  return {
293
312
  id: c.id,
294
313
  body: c.body,
@@ -316,9 +335,9 @@ Examples:
316
335
  const isTerminal = terminalStates.includes(state.name) || state.type === "cancelled";
317
336
  if (!isTerminal) {
318
337
  console.log(`Updating ${issue.identifier} to ${statusTransitions.testing} in Linear...`);
319
- const payload = await client.updateIssue(issue.id, {
338
+ const payload = await withRetry(() => client.updateIssue(issue.id, {
320
339
  stateId: testingStateId,
321
- });
340
+ }), { label: `update ${issue.identifier}` });
322
341
  if (!payload.success) {
323
342
  throw new Error("Linear mutation returned success=false");
324
343
  }
@@ -141,6 +141,10 @@ export declare function getUserEmails(): Promise<string[]>;
141
141
  /** @deprecated Use getUserEmails() instead. Returns first user email for backwards compatibility. */
142
142
  export declare function getUserEmail(): Promise<string>;
143
143
  export declare function getLinearClient(): LinearClient;
144
+ export declare function withRetry<T>(fn: () => Promise<T>, opts?: {
145
+ retries?: number;
146
+ label?: string;
147
+ }): Promise<T>;
144
148
  export declare function loadCycleData(): Promise<CycleData | null>;
145
149
  export declare function saveCycleData(data: CycleData): Promise<void>;
146
150
  export declare function saveConfig(config: Config): Promise<void>;
@@ -133,6 +133,46 @@ export function getLinearClient() {
133
133
  }
134
134
  return new LinearClient({ apiKey });
135
135
  }
136
+ const TRANSIENT_PATTERN = /terminated|socket|ECONN|ETIMEDOUT|ENETUNREACH|ENOTFOUND|EAI_AGAIN|UND_ERR|fetch failed|other side closed|network|timeout|502|503|504/i;
137
+ function isTransientError(err) {
138
+ if (!err)
139
+ return false;
140
+ const e = err;
141
+ const parts = [
142
+ typeof e.message === "string" ? e.message : "",
143
+ typeof e.cause?.message === "string" ? e.cause.message : "",
144
+ typeof e.raw?.message === "string" ? e.raw.message : "",
145
+ typeof e.raw?.cause?.message === "string" ? e.raw.cause.message : "",
146
+ typeof e.type === "string" ? e.type : "",
147
+ ];
148
+ const status = typeof e.status === "number" ? e.status : 0;
149
+ if (status >= 500 && status < 600)
150
+ return true;
151
+ if (status === 429)
152
+ return true;
153
+ return TRANSIENT_PATTERN.test(parts.join(" "));
154
+ }
155
+ export async function withRetry(fn, opts = {}) {
156
+ const retries = opts.retries ?? 4;
157
+ let lastErr;
158
+ for (let attempt = 0; attempt <= retries; attempt++) {
159
+ try {
160
+ return await fn();
161
+ }
162
+ catch (err) {
163
+ lastErr = err;
164
+ if (!isTransientError(err) || attempt === retries)
165
+ throw err;
166
+ const delay = Math.min(2 ** attempt * 500, 8000) + Math.random() * 250;
167
+ const msg = err?.message ?? String(err);
168
+ if (opts.label) {
169
+ process.stdout.write(`\n [retry ${attempt + 1}/${retries}] ${opts.label}: ${msg.slice(0, 120)}\n`);
170
+ }
171
+ await new Promise((r) => setTimeout(r, delay));
172
+ }
173
+ }
174
+ throw lastErr;
175
+ }
136
176
  export async function loadCycleData() {
137
177
  try {
138
178
  await fs.access(CYCLE_PATH);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.7.3",
3
+ "version": "3.7.5",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
@@ -88,3 +88,50 @@ ttt sync → ttt work-on next → ttt estimate <id> <hours> → [implement] →
88
88
  | Issue not found locally | `ttt show <id> --remote` or `ttt sync <id>` (`ttt status` auto-fetches from remote) |
89
89
  | API key not set | Set `LINEAR_API_KEY` or `TRELLO_API_KEY` + `TRELLO_TOKEN` |
90
90
  | Stale data | `ttt sync` to refresh |
91
+
92
+ ## Common Rationalizations
93
+
94
+ | Excuse | Reality |
95
+ |--------|---------|
96
+ | "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
+ | "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
+ | "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
+ | "I know what the task needs without reading it" | Fabricated assumptions produce wrong work. Run `ttt show <id>` before starting. |
101
+ | "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
+ ## Flowchart — Intent Routing
104
+
105
+ ```dot
106
+ digraph ttt_router {
107
+ rankdir=LR;
108
+
109
+ user [label="User mentions\nLinear/Trello", shape=doublecircle];
110
+ classify [label="Classify intent", shape=diamond];
111
+
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];
123
+
124
+ user -> classify;
125
+ classify -> sync [label="fetch / pull"];
126
+ classify -> show [label="show / list"];
127
+ classify -> work_on [label="start / next"];
128
+ classify -> create [label="create / new"];
129
+ classify -> assign [label="assign / reassign"];
130
+ classify -> edit [label="rename / change field"];
131
+ classify -> cancel [label="cancel / abandon"];
132
+ classify -> estimate [label="estimate / Nh"];
133
+ classify -> status [label="current / set status"];
134
+ classify -> comment [label="comment / note"];
135
+ classify -> done [label="done / complete"];
136
+ }
137
+ ```