team-toon-tack 3.7.2 → 3.7.4

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.0"
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.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": "2.7.0",
4
+ "version": "2.8.0",
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
@@ -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) {
@@ -86,56 +86,56 @@ Examples:
86
86
  }
87
87
  // Build excluded labels set
88
88
  const excludedLabels = new Set(localConfig.exclude_labels ?? []);
89
- // Phase 1: Fetch active cycle directly from team
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;
93
- if (!activeCycle) {
94
- console.error("No active cycle found.");
95
- process.exit(1);
96
- }
97
- const cycleId = activeCycle.id;
98
- const cycleName = activeCycle.name ?? `Cycle #${activeCycle.number}`;
99
- const newCycleInfo = {
100
- id: cycleId,
101
- name: cycleName,
102
- start_date: activeCycle.startsAt?.toISOString().split("T")[0] ?? "",
103
- end_date: activeCycle.endsAt?.toISOString().split("T")[0] ?? "",
104
- };
105
- // Check if cycle changed and update config with history
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" });
95
+ let cycleId;
96
+ let cycleName;
106
97
  const existingData = await loadCycleData();
107
- const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
108
- if (oldCycleId && oldCycleId !== cycleId) {
109
- const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? "Unknown";
110
- console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
111
- // Move old cycle to history (avoid duplicates)
112
- if (config.current_cycle) {
113
- config.cycle_history = config.cycle_history ?? [];
114
- // Remove if already exists in history
115
- config.cycle_history = config.cycle_history.filter((c) => c.id !== config.current_cycle?.id);
116
- config.cycle_history.unshift(config.current_cycle);
117
- // Keep only last 10 cycles
118
- if (config.cycle_history.length > 10) {
119
- config.cycle_history = config.cycle_history.slice(0, 10);
98
+ if (activeCycle) {
99
+ cycleId = activeCycle.id;
100
+ cycleName = activeCycle.name ?? `Cycle #${activeCycle.number}`;
101
+ const newCycleInfo = {
102
+ id: cycleId,
103
+ name: cycleName,
104
+ start_date: activeCycle.startsAt?.toISOString().split("T")[0] ?? "",
105
+ end_date: activeCycle.endsAt?.toISOString().split("T")[0] ?? "",
106
+ };
107
+ const oldCycleId = config.current_cycle?.id ?? existingData?.cycleId;
108
+ if (oldCycleId && oldCycleId !== cycleId) {
109
+ const oldCycleName = config.current_cycle?.name ?? existingData?.cycleName ?? "Unknown";
110
+ console.log(`Cycle changed: ${oldCycleName} → ${cycleName}`);
111
+ if (config.current_cycle) {
112
+ config.cycle_history = config.cycle_history ?? [];
113
+ config.cycle_history = config.cycle_history.filter((c) => c.id !== config.current_cycle?.id);
114
+ config.cycle_history.unshift(config.current_cycle);
115
+ if (config.cycle_history.length > 10) {
116
+ config.cycle_history = config.cycle_history.slice(0, 10);
117
+ }
120
118
  }
121
- }
122
- // Update current cycle
123
- config.current_cycle = newCycleInfo;
124
- await saveConfig(config);
125
- console.log("Config updated with new cycle (old cycle saved to history).");
126
- }
127
- else {
128
- // Update current cycle info even if ID unchanged (dates might change)
129
- if (!config.current_cycle || config.current_cycle.id !== cycleId) {
130
119
  config.current_cycle = newCycleInfo;
131
120
  await saveConfig(config);
121
+ console.log("Config updated with new cycle (old cycle saved to history).");
122
+ }
123
+ else {
124
+ if (!config.current_cycle || config.current_cycle.id !== cycleId) {
125
+ config.current_cycle = newCycleInfo;
126
+ await saveConfig(config);
127
+ }
128
+ console.log(`Current cycle: ${cycleName}`);
132
129
  }
133
- console.log(`Current cycle: ${cycleName}`);
130
+ }
131
+ else {
132
+ cycleName = "No Cycle";
133
+ console.log("No active cycle on this team — syncing without cycle filter.");
134
134
  }
135
135
  // Phase 2: Fetch workflow states and get status mappings
136
- const workflowStates = await client.workflowStates({
136
+ const workflowStates = await withRetry(() => client.workflowStates({
137
137
  filter: { team: { id: { eq: teamId } } },
138
- });
138
+ }), { label: "fetch workflow states" });
139
139
  const stateMap = new Map(workflowStates.nodes.map((s) => [s.name, s.id]));
140
140
  // Get status names from config or use defaults
141
141
  const statusTransitions = config.status_transitions || {
@@ -172,9 +172,9 @@ Examples:
172
172
  }
173
173
  if (targetStateId) {
174
174
  try {
175
- const payload = await client.updateIssue(task.linearId, {
175
+ const payload = await withRetry(() => client.updateIssue(task.linearId, {
176
176
  stateId: targetStateId,
177
- });
177
+ }), { label: `update ${task.id}` });
178
178
  if (!payload.success) {
179
179
  throw new Error("Linear mutation returned success=false");
180
180
  }
@@ -208,7 +208,7 @@ Examples:
208
208
  if (singleIssueId) {
209
209
  // Sync single issue by ID
210
210
  console.log(`Fetching issue ${singleIssueId}...`);
211
- const searchResult = await client.searchIssues(singleIssueId);
211
+ const searchResult = await withRetry(() => client.searchIssues(singleIssueId), { label: `search ${singleIssueId}` });
212
212
  const matchingIssue = searchResult.nodes.find((i) => i.identifier === singleIssueId);
213
213
  if (!matchingIssue) {
214
214
  console.error(`Issue ${singleIssueId} not found.`);
@@ -222,22 +222,24 @@ Examples:
222
222
  ? "all statuses"
223
223
  : `${syncStatuses.join("/")} status`;
224
224
  console.log(`Fetching issues (${statusDesc})${labelDesc}...`);
225
- // Build filter - label is optional
225
+ // Build filter - label is optional; cycle is skipped if team has no
226
+ // active cycle (Linear lets teams disable cycles entirely).
226
227
  const issueFilter = {
227
228
  team: { id: { eq: teamId } },
228
- cycle: { id: { eq: cycleId } },
229
229
  };
230
- // Only filter by status if not syncing all
230
+ if (cycleId) {
231
+ issueFilter.cycle = { id: { eq: cycleId } };
232
+ }
231
233
  if (!syncAll) {
232
234
  issueFilter.state = { name: { in: syncStatuses } };
233
235
  }
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
  }
@@ -372,7 +391,7 @@ Examples:
372
391
  finalTasks = tasks;
373
392
  }
374
393
  const newData = {
375
- cycleId: cycleId,
394
+ cycleId: cycleId ?? `team:${teamId}`,
376
395
  cycleName: cycleName,
377
396
  updatedAt: new Date().toISOString(),
378
397
  tasks: finalTasks,
@@ -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.2",
3
+ "version": "3.7.4",
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
+ ```