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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/commands/ttt-work-on.md +54 -16
- package/commands/ttt-write-work-on-skill.md +43 -40
- package/dist/scripts/sync.js +80 -61
- package/dist/scripts/utils.d.ts +4 -0
- package/dist/scripts/utils.js +40 -0
- package/package.json +1 -1
- package/skills/managing-linear-tasks/SKILL.md +47 -0
|
@@ -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.
|
|
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.
|
|
16
|
+
"version": "2.8.0"
|
|
17
17
|
}
|
|
18
18
|
]
|
|
19
19
|
}
|
package/commands/ttt-work-on.md
CHANGED
|
@@ -48,30 +48,68 @@ Options:
|
|
|
48
48
|
--dry-run Pick task without changing status (preview only)
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## Workflow — Plan → Test → Code → Review
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
56
|
+
### 1. Branch
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
/ttt:done -m "completion summary"
|
|
58
|
+
```bash
|
|
59
|
+
git checkout -b <suggested-branch-name>
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
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
|
-
|
|
106
|
+
A task is NOT complete until `/ttt:done` runs.
|
|
107
|
+
This is MANDATORY.
|
|
69
108
|
|
|
70
|
-
|
|
109
|
+
## Project-Specific Skill
|
|
71
110
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
57
|
+
Follow Plan → Test → Code → Review for every task.
|
|
58
|
+
Skipping phases is debt, not pragmatism.
|
|
58
59
|
|
|
59
|
-
##
|
|
60
|
+
## 1. Plan
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
65
|
+
Branch naming:
|
|
66
|
+
- `feature/<issue-id>-<slug>`
|
|
67
|
+
- `fix/<issue-id>-<slug>`
|
|
68
|
+
- `refactor/<issue-id>-<slug>`
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
- Ensure dependencies are installed
|
|
74
|
-
- Check for required environment variables
|
|
70
|
+
## 2. Test First (TDD)
|
|
75
71
|
|
|
76
|
-
|
|
72
|
+
Invoke `superpowers:test-driven-development`.
|
|
73
|
+
No production code without a failing test first.
|
|
77
74
|
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
Red → Green → Refactor:
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
{{ test-command }}
|
|
78
|
+
\`\`\`
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
{{ file-structure-notes }}
|
|
84
|
+
## 3. Code
|
|
86
85
|
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
\`\`\`bash
|
|
91
|
-
{{ combined-validation-command }}
|
|
92
|
-
\`\`\`
|
|
90
|
+
## 4. Review (before `/ttt:done`)
|
|
93
91
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<type>(<scope>): <description>
|
|
92
|
+
Invoke `superpowers:verification-before-completion`.
|
|
93
|
+
Run every command and confirm the actual output.
|
|
97
94
|
|
|
98
|
-
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
{{ lint-command }}
|
|
97
|
+
{{ type-check-command }}
|
|
98
|
+
{{ test-command }}
|
|
99
99
|
\`\`\`
|
|
100
100
|
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
package/dist/scripts/sync.js
CHANGED
|
@@ -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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
257
|
-
const
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
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,
|
package/dist/scripts/utils.d.ts
CHANGED
|
@@ -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>;
|
package/dist/scripts/utils.js
CHANGED
|
@@ -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
|
@@ -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
|
+
```
|