team-toon-tack 3.2.12 → 3.2.14
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/README.md +16 -1
- package/README.zh-TW.md +16 -1
- package/commands/ttt:estimate.md +68 -0
- package/dist/bin/cli.js +7 -0
- package/dist/scripts/estimate.d.ts +2 -0
- package/dist/scripts/estimate.js +139 -0
- package/dist/scripts/lib/display.js +8 -0
- package/dist/scripts/lib/sync.js +7 -2
- package/dist/scripts/show.js +12 -1
- package/dist/scripts/status.js +49 -3
- package/dist/scripts/sync.js +7 -5
- package/dist/scripts/utils.d.ts +7 -0
- package/dist/scripts/utils.js +10 -0
- package/package.json +1 -1
- package/skills/managing-linear-tasks/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Optimized task workflow for Claude Code — supports Linear and Trello, saves si
|
|
|
15
15
|
- **QA Team Support** — Auto-update parent issues in QA team to "Testing" when completing dev tasks (Linear)
|
|
16
16
|
- **Attachment Download** — Auto-download images and files to local `.ttt/output/` for AI vision analysis
|
|
17
17
|
- **Blocked Status** — Set tasks as blocked when waiting on external dependencies
|
|
18
|
+
- **Persistent Estimates** — Store local human-effort estimates that survive future `ttt sync`
|
|
18
19
|
- **Claude Code Plugin** — Install plugin for `/ttt:*` commands and auto-activated skills
|
|
19
20
|
- **Cycle History** — Local `.toon` files preserve cycle data for AI context
|
|
20
21
|
|
|
@@ -76,6 +77,7 @@ In Claude Code (with plugin installed):
|
|
|
76
77
|
```
|
|
77
78
|
/ttt:sync # Fetch all issues/cards for current cycle
|
|
78
79
|
/ttt:work-on next # Pick highest priority task & start working
|
|
80
|
+
/ttt:estimate MP-123 6 # Save a local 6-hour estimate
|
|
79
81
|
/ttt:done # Complete task with AI-generated summary
|
|
80
82
|
```
|
|
81
83
|
|
|
@@ -84,6 +86,7 @@ Or using CLI directly:
|
|
|
84
86
|
```bash
|
|
85
87
|
ttt sync
|
|
86
88
|
ttt work-on next
|
|
89
|
+
ttt estimate MP-123 6
|
|
87
90
|
ttt done -m "Completed the task"
|
|
88
91
|
```
|
|
89
92
|
|
|
@@ -125,6 +128,17 @@ ttt work-on MP-123 # Specific issue
|
|
|
125
128
|
ttt work-on next # Auto-select highest priority
|
|
126
129
|
```
|
|
127
130
|
|
|
131
|
+
### `ttt estimate`
|
|
132
|
+
|
|
133
|
+
Save a local human-effort estimate that persists in `.ttt/cycle.toon`.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
ttt estimate MP-123 6 # Save a 6-hour estimate
|
|
137
|
+
ttt estimate 2.5 # Save estimate for current in-progress task
|
|
138
|
+
ttt estimate MP-123 16 --note "API pending" # Save estimate with a note
|
|
139
|
+
ttt estimate MP-123 --clear # Remove saved estimate
|
|
140
|
+
```
|
|
141
|
+
|
|
128
142
|
### `ttt done`
|
|
129
143
|
|
|
130
144
|
Mark task as completed.
|
|
@@ -184,7 +198,7 @@ your-project/
|
|
|
184
198
|
└── .ttt/
|
|
185
199
|
├── config.toon # Team config (gitignore recommended)
|
|
186
200
|
├── local.toon # Personal settings (gitignore)
|
|
187
|
-
├── cycle.toon # Current cycle data (auto-generated)
|
|
201
|
+
├── cycle.toon # Current cycle data + local estimates (auto-generated)
|
|
188
202
|
└── output/ # Downloaded attachments (images, files)
|
|
189
203
|
```
|
|
190
204
|
|
|
@@ -229,6 +243,7 @@ Install the plugin for Claude Code integration:
|
|
|
229
243
|
|---------|-------------|
|
|
230
244
|
| `/ttt:sync` | Sync issues to local cycle data |
|
|
231
245
|
| `/ttt:work-on` | Start working on a task |
|
|
246
|
+
| `/ttt:estimate` | Save a local human-effort estimate |
|
|
232
247
|
| `/ttt:done` | Mark current task as completed |
|
|
233
248
|
| `/ttt:status` | Show or modify task status |
|
|
234
249
|
| `/ttt:show` | Show issue details or search issues |
|
package/README.zh-TW.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- **QA 團隊支援** — 完成開發任務時自動將 QA 團隊的 parent issue 更新為「Testing」(Linear)
|
|
16
16
|
- **附件下載** — 自動下載圖片和檔案到本地 `.ttt/output/`,供 AI 視覺分析
|
|
17
17
|
- **阻塞狀態** — 等待外部依賴時可設定任務為 blocked
|
|
18
|
+
- **持久化估時** — 把人類工程工時估算寫進本地資料,後續 `ttt sync` 不會洗掉
|
|
18
19
|
- **Claude Code Plugin** — 安裝 plugin 即可使用 `/ttt:*` 指令和自動啟用的技能
|
|
19
20
|
- **Cycle 歷史保存** — 本地 `.toon` 檔案保留 cycle 資料,方便 AI 檢閱
|
|
20
21
|
|
|
@@ -76,6 +77,7 @@ ttt init
|
|
|
76
77
|
```
|
|
77
78
|
/ttt:sync # 取得當前 cycle 所有 issue/card
|
|
78
79
|
/ttt:work-on next # 挑選最高優先級任務並開始工作
|
|
80
|
+
/ttt:estimate MP-123 6 # 寫入本地 6 小時估時
|
|
79
81
|
/ttt:done # 完成任務,附上 AI 生成的摘要
|
|
80
82
|
```
|
|
81
83
|
|
|
@@ -84,6 +86,7 @@ ttt init
|
|
|
84
86
|
```bash
|
|
85
87
|
ttt sync
|
|
86
88
|
ttt work-on next
|
|
89
|
+
ttt estimate MP-123 6
|
|
87
90
|
ttt done -m "完成任務"
|
|
88
91
|
```
|
|
89
92
|
|
|
@@ -125,6 +128,17 @@ ttt work-on MP-123 # 指定 issue
|
|
|
125
128
|
ttt work-on next # 自動選擇最高優先級
|
|
126
129
|
```
|
|
127
130
|
|
|
131
|
+
### `ttt estimate`
|
|
132
|
+
|
|
133
|
+
把人類工程工時估算寫進 `.ttt/cycle.toon`,並在之後的同步中保留下來。
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
ttt estimate MP-123 6 # 寫入 6 小時估時
|
|
137
|
+
ttt estimate 2.5 # 對目前進行中的任務寫入 2.5 小時
|
|
138
|
+
ttt estimate MP-123 16 --note "API pending" # 連同註記一起保存
|
|
139
|
+
ttt estimate MP-123 --clear # 移除既有估時
|
|
140
|
+
```
|
|
141
|
+
|
|
128
142
|
### `ttt done`
|
|
129
143
|
|
|
130
144
|
標記任務完成。
|
|
@@ -184,7 +198,7 @@ your-project/
|
|
|
184
198
|
└── .ttt/
|
|
185
199
|
├── config.toon # 團隊配置(建議 gitignore)
|
|
186
200
|
├── local.toon # 個人設定(gitignore)
|
|
187
|
-
├── cycle.toon # 當前 cycle
|
|
201
|
+
├── cycle.toon # 當前 cycle 資料與本地估時(自動產生)
|
|
188
202
|
└── output/ # 下載的附件(圖片、檔案)
|
|
189
203
|
```
|
|
190
204
|
|
|
@@ -229,6 +243,7 @@ your-project/
|
|
|
229
243
|
|------|------|
|
|
230
244
|
| `/ttt:sync` | 同步 issue 到本地 |
|
|
231
245
|
| `/ttt:work-on` | 開始處理任務 |
|
|
246
|
+
| `/ttt:estimate` | 寫入本地人力估時 |
|
|
232
247
|
| `/ttt:done` | 標記當前任務完成 |
|
|
233
248
|
| `/ttt:status` | 顯示或修改任務狀態 |
|
|
234
249
|
| `/ttt:show` | 顯示 issue 詳情或搜尋 issue |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ttt:estimate
|
|
3
|
+
description: Save a local human-effort estimate for a task
|
|
4
|
+
arguments:
|
|
5
|
+
- name: issue-id
|
|
6
|
+
description: Issue ID to estimate (e.g., MP-624)
|
|
7
|
+
required: false
|
|
8
|
+
- name: hours
|
|
9
|
+
description: Estimated human engineer hours (supports decimals)
|
|
10
|
+
required: false
|
|
11
|
+
- name: note
|
|
12
|
+
description: Short estimate note or assumption summary
|
|
13
|
+
required: false
|
|
14
|
+
- name: clear
|
|
15
|
+
description: Remove the existing estimate from the task
|
|
16
|
+
required: false
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<law>
|
|
20
|
+
YOU MUST execute the `ttt estimate` command using the Bash tool.
|
|
21
|
+
DO NOT manually edit cycle.toon — the CLI handles persistence safely.
|
|
22
|
+
DO NOT put agent runtime here; store human engineer implementation effort only.
|
|
23
|
+
</law>
|
|
24
|
+
|
|
25
|
+
# /ttt:estimate — Save Task Estimate
|
|
26
|
+
|
|
27
|
+
## Execution
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ttt estimate {{ issue-id }} {{ hours }} {{ "--note \"" + note + "\"" if note }} {{ "--clear" if clear }}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Common Examples
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
ttt estimate MP-624 6
|
|
37
|
+
ttt estimate 2.5
|
|
38
|
+
ttt estimate MP-624 16 --note "backend contract pending"
|
|
39
|
+
ttt estimate MP-624 --clear
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Full CLI Reference
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
Usage: ttt estimate [issue-id] [hours] [options]
|
|
46
|
+
|
|
47
|
+
Arguments:
|
|
48
|
+
issue-id Optional. Issue ID (e.g., MP-624)
|
|
49
|
+
hours Estimated human engineer hours (supports decimals)
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
-n, --note <text> Short estimate note or assumption summary
|
|
53
|
+
--clear Remove existing estimate from the task
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What It Does
|
|
57
|
+
|
|
58
|
+
- Stores the estimate in local `.ttt/cycle.toon`
|
|
59
|
+
- Preserves that estimate across future `ttt sync` runs
|
|
60
|
+
- Lets `ttt show` and `ttt status` display the saved estimate
|
|
61
|
+
|
|
62
|
+
## Error Handling
|
|
63
|
+
|
|
64
|
+
| Error | Solution |
|
|
65
|
+
|-------|----------|
|
|
66
|
+
| `No cycle data found` | Run `ttt sync` first |
|
|
67
|
+
| `No in-progress task found` | Specify `issue-id` explicitly |
|
|
68
|
+
| `Estimated hours must be a positive number` | Pass a number like `2`, `4.5`, or `16` |
|
package/dist/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const COMMANDS = [
|
|
|
10
10
|
"init",
|
|
11
11
|
"sync",
|
|
12
12
|
"work-on",
|
|
13
|
+
"estimate",
|
|
13
14
|
"done",
|
|
14
15
|
"status",
|
|
15
16
|
"show",
|
|
@@ -28,6 +29,7 @@ COMMANDS:
|
|
|
28
29
|
init Initialize config files in current directory
|
|
29
30
|
sync Sync issues from Linear to local cycle.ttt
|
|
30
31
|
work-on Start working on a task (interactive or by ID)
|
|
32
|
+
estimate Store a local human-effort estimate for a task
|
|
31
33
|
done Mark current task as completed
|
|
32
34
|
status Show or modify task status
|
|
33
35
|
show Show issue details or search issues by filters
|
|
@@ -46,6 +48,7 @@ EXAMPLES:
|
|
|
46
48
|
ttt work-on # Interactive task selection
|
|
47
49
|
ttt work-on MP-123 # Work on specific issue
|
|
48
50
|
ttt work-on next # Auto-select highest priority
|
|
51
|
+
ttt estimate MP-123 6 # Save a 6-hour estimate locally
|
|
49
52
|
ttt done # Complete current task
|
|
50
53
|
ttt done -m "Fixed the bug" # With completion message
|
|
51
54
|
ttt show MP-123 # Show issue from local data
|
|
@@ -114,6 +117,10 @@ async function main() {
|
|
|
114
117
|
process.argv = ["node", "work-on.js", ...commandArgs];
|
|
115
118
|
await import(`${scriptDir}work-on.js`);
|
|
116
119
|
break;
|
|
120
|
+
case "estimate":
|
|
121
|
+
process.argv = ["node", "estimate.js", ...commandArgs];
|
|
122
|
+
await import(`${scriptDir}estimate.js`);
|
|
123
|
+
break;
|
|
117
124
|
case "done":
|
|
118
125
|
process.argv = ["node", "done-job.js", ...commandArgs];
|
|
119
126
|
await import(`${scriptDir}done-job.js`);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { input, select } from "@inquirer/prompts";
|
|
3
|
+
import { loadCycleData, saveCycleData } from "./utils.js";
|
|
4
|
+
function parseEstimateArgs(args) {
|
|
5
|
+
const parsed = { clear: false };
|
|
6
|
+
for (let i = 0; i < args.length; i++) {
|
|
7
|
+
const arg = args[i];
|
|
8
|
+
if (arg === "--note" || arg === "-n") {
|
|
9
|
+
parsed.note = args[++i];
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (arg === "--clear") {
|
|
13
|
+
parsed.clear = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (arg.startsWith("-")) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!parsed.issueId && !/^\d+(\.\d+)?$/.test(arg)) {
|
|
20
|
+
parsed.issueId = arg;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (parsed.hours === undefined) {
|
|
24
|
+
const hours = Number(arg);
|
|
25
|
+
if (!Number.isNaN(hours)) {
|
|
26
|
+
parsed.hours = hours;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
function findTask(tasks, issueId) {
|
|
33
|
+
if (issueId) {
|
|
34
|
+
return tasks.find((task) => task.id === issueId || task.id === `MP-${issueId}`);
|
|
35
|
+
}
|
|
36
|
+
const inProgressTasks = tasks.filter((task) => task.localStatus === "in-progress");
|
|
37
|
+
if (inProgressTasks.length === 1) {
|
|
38
|
+
return inProgressTasks[0];
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
async function resolveTask(tasks, issueId) {
|
|
43
|
+
const directMatch = findTask(tasks, issueId);
|
|
44
|
+
if (directMatch) {
|
|
45
|
+
return directMatch;
|
|
46
|
+
}
|
|
47
|
+
if (issueId) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const inProgressTasks = tasks.filter((task) => task.localStatus === "in-progress");
|
|
51
|
+
if (inProgressTasks.length === 0) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
if (!process.stdin.isTTY) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const selectedId = await select({
|
|
58
|
+
message: "選擇要寫入估時的任務:",
|
|
59
|
+
choices: inProgressTasks.map((task) => ({
|
|
60
|
+
name: `${task.id}: ${task.title}`,
|
|
61
|
+
value: task.id,
|
|
62
|
+
description: task.estimate
|
|
63
|
+
? `目前 ${task.estimate.hours}h`
|
|
64
|
+
: "尚未寫入 estimate",
|
|
65
|
+
})),
|
|
66
|
+
});
|
|
67
|
+
return tasks.find((task) => task.id === selectedId);
|
|
68
|
+
}
|
|
69
|
+
async function resolveHours(hours) {
|
|
70
|
+
if (hours !== undefined) {
|
|
71
|
+
return hours;
|
|
72
|
+
}
|
|
73
|
+
if (!process.stdin.isTTY) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const answer = await input({
|
|
77
|
+
message: "預估工時(小時,可含小數):",
|
|
78
|
+
});
|
|
79
|
+
const parsed = Number(answer);
|
|
80
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
81
|
+
}
|
|
82
|
+
async function estimate() {
|
|
83
|
+
const args = process.argv.slice(2);
|
|
84
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
85
|
+
console.log(`Usage: ttt estimate [issue-id] [hours] [options]
|
|
86
|
+
|
|
87
|
+
Store a local human-effort estimate in cycle.toon.
|
|
88
|
+
|
|
89
|
+
Arguments:
|
|
90
|
+
issue-id Optional. Issue ID (e.g., MP-624). Defaults to current in-progress task.
|
|
91
|
+
hours Estimated hours as a human engineer (supports decimals)
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
-n, --note <text> Short note or assumption summary
|
|
95
|
+
--clear Remove existing estimate from the task
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
ttt estimate MP-624 6
|
|
99
|
+
ttt estimate 2.5
|
|
100
|
+
ttt estimate MP-624 16 -n "backend contract pending"
|
|
101
|
+
ttt estimate MP-624 --clear`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
const { issueId, hours, note, clear } = parseEstimateArgs(args);
|
|
105
|
+
const data = await loadCycleData();
|
|
106
|
+
if (!data) {
|
|
107
|
+
console.error("No cycle data found. Run ttt sync first.");
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const task = await resolveTask(data.tasks, issueId);
|
|
111
|
+
if (!task) {
|
|
112
|
+
console.error(issueId
|
|
113
|
+
? `Issue ${issueId} not found in local data.`
|
|
114
|
+
: "No in-progress task found. Specify issue ID explicitly.");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (clear) {
|
|
118
|
+
delete task.estimate;
|
|
119
|
+
await saveCycleData(data);
|
|
120
|
+
console.log(`Local: ${task.id} estimate cleared`);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
const resolvedHours = await resolveHours(hours);
|
|
124
|
+
if (resolvedHours === undefined ||
|
|
125
|
+
Number.isNaN(resolvedHours) ||
|
|
126
|
+
resolvedHours <= 0) {
|
|
127
|
+
console.error("Estimated hours must be a positive number.");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
task.estimate = {
|
|
131
|
+
hours: resolvedHours,
|
|
132
|
+
note: note || undefined,
|
|
133
|
+
updatedAt: new Date().toISOString(),
|
|
134
|
+
};
|
|
135
|
+
await saveCycleData(data);
|
|
136
|
+
const suffix = task.estimate.note ? ` (${task.estimate.note})` : "";
|
|
137
|
+
console.log(`Local: ${task.id} estimate → ${task.estimate.hours}h${suffix}`);
|
|
138
|
+
}
|
|
139
|
+
estimate().catch(console.error);
|
|
@@ -30,6 +30,10 @@ export function displayTaskInfo(task) {
|
|
|
30
30
|
console.log(`Labels: ${task.labels.join(", ")}`);
|
|
31
31
|
if (task.assignee)
|
|
32
32
|
console.log(`Assignee: ${task.assignee}`);
|
|
33
|
+
if (task.estimate) {
|
|
34
|
+
const suffix = task.estimate.note ? ` (${task.estimate.note})` : "";
|
|
35
|
+
console.log(`Estimate: ${task.estimate.hours}h${suffix}`);
|
|
36
|
+
}
|
|
33
37
|
if (task.url)
|
|
34
38
|
console.log(`URL: ${task.url}`);
|
|
35
39
|
}
|
|
@@ -93,6 +97,10 @@ export function displayTaskWithStatus(task) {
|
|
|
93
97
|
console.log(` Priority: ${PRIORITY_LABELS[task.priority] || "None"}`);
|
|
94
98
|
console.log(` Labels: ${task.labels.join(", ")}`);
|
|
95
99
|
console.log(` Assignee: ${task.assignee || "Unassigned"}`);
|
|
100
|
+
if (task.estimate) {
|
|
101
|
+
const suffix = task.estimate.note ? ` (${task.estimate.note})` : "";
|
|
102
|
+
console.log(` Estimate: ${task.estimate.hours}h${suffix}`);
|
|
103
|
+
}
|
|
96
104
|
if (task.url)
|
|
97
105
|
console.log(` URL: ${task.url}`);
|
|
98
106
|
displayTaskDescription(task);
|
package/dist/scripts/lib/sync.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getLinearClient, getPrioritySortIndex, loadCycleData, saveCycleData, } from "../utils.js";
|
|
1
|
+
import { getLinearClient, getPrioritySortIndex, loadCycleData, preserveLocalTaskFields, saveCycleData, } from "../utils.js";
|
|
2
2
|
import { getStatusTransitions } from "./linear.js";
|
|
3
3
|
import { mapRemoteToLocalStatus } from "./status-helpers.js";
|
|
4
4
|
/**
|
|
@@ -65,7 +65,7 @@ export async function syncSingleIssue(issueId, options) {
|
|
|
65
65
|
const { config, localConfig, preserveLocalStatus = true, } = options;
|
|
66
66
|
const client = options.client ?? getLinearClient();
|
|
67
67
|
// Fetch issue details using shared function
|
|
68
|
-
|
|
68
|
+
let task = await fetchIssueDetail(issueId, { client });
|
|
69
69
|
if (!task) {
|
|
70
70
|
console.error(`Issue ${issueId} not found in Linear.`);
|
|
71
71
|
return null;
|
|
@@ -76,6 +76,7 @@ export async function syncSingleIssue(issueId, options) {
|
|
|
76
76
|
const existingTask = existingData.tasks.find((t) => t.id === issueId);
|
|
77
77
|
if (existingTask) {
|
|
78
78
|
task.localStatus = existingTask.localStatus;
|
|
79
|
+
task = preserveLocalTaskFields(task, existingTask);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
// Map remote status to local status if not preserving
|
|
@@ -83,6 +84,10 @@ export async function syncSingleIssue(issueId, options) {
|
|
|
83
84
|
const transitions = getStatusTransitions(config);
|
|
84
85
|
task.localStatus = mapRemoteToLocalStatus(task.status, transitions, localConfig);
|
|
85
86
|
}
|
|
87
|
+
if (existingData) {
|
|
88
|
+
const existingTask = existingData.tasks.find((t) => t.id === issueId);
|
|
89
|
+
task = preserveLocalTaskFields(task, existingTask);
|
|
90
|
+
}
|
|
86
91
|
// Update cycle data
|
|
87
92
|
if (existingData) {
|
|
88
93
|
const existingTasks = existingData.tasks.filter((t) => t.id !== issueId);
|
package/dist/scripts/show.js
CHANGED
|
@@ -10,6 +10,10 @@ function taskToMarkdown(task) {
|
|
|
10
10
|
lines.push(`- **Priority**: ${priority}`);
|
|
11
11
|
lines.push(`- **Labels**: ${task.labels.length > 0 ? task.labels.join(", ") : "-"}`);
|
|
12
12
|
lines.push(`- **Assignee**: ${task.assignee || "Unassigned"}`);
|
|
13
|
+
if (task.estimate) {
|
|
14
|
+
const note = task.estimate.note ? ` (${task.estimate.note})` : "";
|
|
15
|
+
lines.push(`- **Estimate**: ${task.estimate.hours}h${note}`);
|
|
16
|
+
}
|
|
13
17
|
if (task.url)
|
|
14
18
|
lines.push(`- **URL**: ${task.url}`);
|
|
15
19
|
if (task.parentIssueId)
|
|
@@ -60,6 +64,10 @@ function tasksToMarkdownList(tasks) {
|
|
|
60
64
|
lines.push(`| Status | ${task.status} |`);
|
|
61
65
|
lines.push(`| Priority | ${priority} |`);
|
|
62
66
|
lines.push(`| Assignee | ${assignee} |`);
|
|
67
|
+
if (task.estimate) {
|
|
68
|
+
const note = task.estimate.note ? ` (${task.estimate.note})` : "";
|
|
69
|
+
lines.push(`| Estimate | ${task.estimate.hours}h${note} |`);
|
|
70
|
+
}
|
|
63
71
|
lines.push(`| Labels | ${task.labels.length > 0 ? task.labels.join(", ") : "-"} |`);
|
|
64
72
|
if (task.url)
|
|
65
73
|
lines.push(`| URL | ${task.url} |`);
|
|
@@ -79,8 +87,11 @@ function displayTaskList(tasks) {
|
|
|
79
87
|
const priority = PRIORITY_LABELS[task.priority] || "⚪ None";
|
|
80
88
|
const assignee = task.assignee ? task.assignee.split("@")[0] : "unassigned";
|
|
81
89
|
const labels = task.labels.length > 0 ? task.labels.join(", ") : "-";
|
|
90
|
+
const estimate = task.estimate
|
|
91
|
+
? ` | Estimate: ${task.estimate.hours}h`
|
|
92
|
+
: "";
|
|
82
93
|
console.log(`${icon} ${task.id}: ${task.title}`);
|
|
83
|
-
console.log(` Status: ${task.status} | Priority: ${priority} | Assignee: ${assignee}`);
|
|
94
|
+
console.log(` Status: ${task.status} | Priority: ${priority} | Assignee: ${assignee}${estimate}`);
|
|
84
95
|
console.log(` Labels: ${labels}`);
|
|
85
96
|
console.log("─".repeat(80));
|
|
86
97
|
}
|
package/dist/scripts/status.js
CHANGED
|
@@ -24,6 +24,40 @@ function parseArgs(args) {
|
|
|
24
24
|
}
|
|
25
25
|
return { issueId, setStatus };
|
|
26
26
|
}
|
|
27
|
+
async function fetchTaskFromRemote(issueId, config) {
|
|
28
|
+
const adapter = createAdapter(config);
|
|
29
|
+
const sourceType = getSourceType(config);
|
|
30
|
+
const issue = await adapter.searchIssue(issueId);
|
|
31
|
+
if (!issue)
|
|
32
|
+
return undefined;
|
|
33
|
+
return {
|
|
34
|
+
id: issue.id,
|
|
35
|
+
linearId: issue.sourceId,
|
|
36
|
+
sourceId: issue.sourceId,
|
|
37
|
+
sourceType,
|
|
38
|
+
title: issue.title,
|
|
39
|
+
status: issue.status,
|
|
40
|
+
localStatus: "pending",
|
|
41
|
+
assignee: issue.assigneeEmail,
|
|
42
|
+
priority: issue.priority,
|
|
43
|
+
labels: issue.labels,
|
|
44
|
+
description: issue.description,
|
|
45
|
+
parentIssueId: issue.parentIssueId,
|
|
46
|
+
url: issue.url,
|
|
47
|
+
attachments: issue.attachments?.map((a) => ({
|
|
48
|
+
id: a.id,
|
|
49
|
+
title: a.title,
|
|
50
|
+
url: a.url,
|
|
51
|
+
sourceType: a.sourceType,
|
|
52
|
+
})),
|
|
53
|
+
comments: issue.comments?.map((c) => ({
|
|
54
|
+
id: c.id,
|
|
55
|
+
body: c.body,
|
|
56
|
+
createdAt: c.createdAt,
|
|
57
|
+
user: c.user,
|
|
58
|
+
})),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
27
61
|
async function status() {
|
|
28
62
|
const args = process.argv.slice(2);
|
|
29
63
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -65,13 +99,15 @@ Examples:
|
|
|
65
99
|
// Find task
|
|
66
100
|
let task;
|
|
67
101
|
let issueId = argIssueId;
|
|
102
|
+
let fromRemote = false;
|
|
68
103
|
if (!issueId) {
|
|
69
104
|
const inProgressTasks = data.tasks.filter((t) => t.localStatus === "in-progress");
|
|
70
105
|
if (inProgressTasks.length === 0) {
|
|
71
106
|
console.log("No in-progress task found.");
|
|
72
107
|
console.log("\nAll tasks:");
|
|
73
108
|
for (const t of data.tasks) {
|
|
74
|
-
|
|
109
|
+
const estimate = t.estimate ? ` | ${t.estimate.hours}h` : "";
|
|
110
|
+
console.log(` ${getStatusIcon(t.localStatus)} ${t.id}: ${t.title} [${t.localStatus}]${estimate}`);
|
|
75
111
|
}
|
|
76
112
|
process.exit(0);
|
|
77
113
|
}
|
|
@@ -81,8 +117,15 @@ Examples:
|
|
|
81
117
|
else {
|
|
82
118
|
task = data.tasks.find((t) => t.id === issueId || t.id === `MP-${issueId}`);
|
|
83
119
|
if (!task) {
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
// Fetch from remote
|
|
121
|
+
console.error(`Issue ${issueId} not in local data. Fetching from remote...`);
|
|
122
|
+
task = await fetchTaskFromRemote(issueId, config);
|
|
123
|
+
if (!task) {
|
|
124
|
+
console.error(`Issue ${issueId} not found.`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
issueId = task.id;
|
|
128
|
+
fromRemote = true;
|
|
86
129
|
}
|
|
87
130
|
}
|
|
88
131
|
// If setting status
|
|
@@ -181,6 +224,9 @@ Examples:
|
|
|
181
224
|
}
|
|
182
225
|
// Save if anything changed
|
|
183
226
|
if (needsSave) {
|
|
227
|
+
if (fromRemote) {
|
|
228
|
+
data.tasks.push(task);
|
|
229
|
+
}
|
|
184
230
|
await saveCycleData(data);
|
|
185
231
|
if (newLocalStatus && newLocalStatus !== oldLocalStatus) {
|
|
186
232
|
console.log(`Local: ${task.id} ${oldLocalStatus} → ${newLocalStatus}`);
|
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, saveConfig, saveCycleData, } from "./utils.js";
|
|
4
|
+
import { getLinearClient, getPaths, getPrioritySortIndex, getSourceType, getTeamId, loadConfig, loadCycleData, loadLocalConfig, preserveLocalTaskFields, saveConfig, saveCycleData, } 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) {
|
|
@@ -332,7 +332,8 @@ Examples:
|
|
|
332
332
|
localStatus = "completed";
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
|
-
const
|
|
335
|
+
const existingTask = existingTasksMap.get(issue.identifier);
|
|
336
|
+
const task = preserveLocalTaskFields({
|
|
336
337
|
id: issue.identifier,
|
|
337
338
|
linearId: issue.id,
|
|
338
339
|
title: issue.title,
|
|
@@ -346,7 +347,7 @@ Examples:
|
|
|
346
347
|
url: issue.url,
|
|
347
348
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
348
349
|
comments: comments.length > 0 ? comments : undefined,
|
|
349
|
-
};
|
|
350
|
+
}, existingTask);
|
|
350
351
|
tasks.push(task);
|
|
351
352
|
}
|
|
352
353
|
// Sort by priority using config order
|
|
@@ -545,7 +546,8 @@ async function syncTrello(config, localConfig, options) {
|
|
|
545
546
|
else {
|
|
546
547
|
localStatus = resolveLocalStatus(undefined, issue.status, statusTransitions, localConfig);
|
|
547
548
|
}
|
|
548
|
-
const
|
|
549
|
+
const existingTask = existingTasksMap.get(issue.id);
|
|
550
|
+
const task = preserveLocalTaskFields({
|
|
549
551
|
id: issue.id,
|
|
550
552
|
linearId: issue.sourceId, // For backwards compatibility
|
|
551
553
|
sourceId: issue.sourceId,
|
|
@@ -561,7 +563,7 @@ async function syncTrello(config, localConfig, options) {
|
|
|
561
563
|
url: issue.url,
|
|
562
564
|
attachments: attachments.length > 0 ? attachments : undefined,
|
|
563
565
|
comments: comments.length > 0 ? comments : undefined,
|
|
564
|
-
};
|
|
566
|
+
}, existingTask);
|
|
565
567
|
tasks.push(task);
|
|
566
568
|
}
|
|
567
569
|
// Sort by priority
|
package/dist/scripts/utils.d.ts
CHANGED
|
@@ -82,6 +82,11 @@ export interface Comment {
|
|
|
82
82
|
createdAt: string;
|
|
83
83
|
user?: string;
|
|
84
84
|
}
|
|
85
|
+
export interface TaskEstimate {
|
|
86
|
+
hours: number;
|
|
87
|
+
note?: string;
|
|
88
|
+
updatedAt: string;
|
|
89
|
+
}
|
|
85
90
|
export interface Task {
|
|
86
91
|
id: string;
|
|
87
92
|
/** @deprecated Use sourceId instead. Kept for backwards compatibility. */
|
|
@@ -103,6 +108,7 @@ export interface Task {
|
|
|
103
108
|
url?: string;
|
|
104
109
|
attachments?: Attachment[];
|
|
105
110
|
comments?: Comment[];
|
|
111
|
+
estimate?: TaskEstimate;
|
|
106
112
|
}
|
|
107
113
|
export interface CycleData {
|
|
108
114
|
cycleId: string;
|
|
@@ -135,3 +141,4 @@ export declare function getDefaultTeamKey(config: Config): string;
|
|
|
135
141
|
export declare function getTeamId(config: Config, teamKey?: string): string;
|
|
136
142
|
export declare function getSourceType(config: Config): TaskSourceType;
|
|
137
143
|
export declare function getTaskSourceId(task: Task): string;
|
|
144
|
+
export declare function preserveLocalTaskFields(task: Task, existingTask?: Task): Task;
|
package/dist/scripts/utils.js
CHANGED
|
@@ -151,3 +151,13 @@ export function getSourceType(config) {
|
|
|
151
151
|
export function getTaskSourceId(task) {
|
|
152
152
|
return task.sourceId ?? task.linearId;
|
|
153
153
|
}
|
|
154
|
+
export function preserveLocalTaskFields(task, existingTask) {
|
|
155
|
+
if (!existingTask) {
|
|
156
|
+
return task;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
...task,
|
|
160
|
+
branch: existingTask.branch ?? task.branch,
|
|
161
|
+
estimate: existingTask.estimate ?? task.estimate,
|
|
162
|
+
};
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ Match user intent to the correct `/ttt:*` command:
|
|
|
23
23
|
| Sync/fetch issues | `/ttt:sync` | "sync my issues", "pull from Linear" |
|
|
24
24
|
| Show/search issues | `/ttt:show` | "show MP-624", "list my tasks", "what issues do I have" |
|
|
25
25
|
| Start working on a task | `/ttt:work-on` | "work on next", "start MP-624" |
|
|
26
|
+
| Record an estimate | `/ttt:estimate` | "estimate MP-624 as 6h", "save 2.5h estimate" |
|
|
26
27
|
| Check/change status | `/ttt:status` | "what's my current task", "set MP-624 to blocked" |
|
|
27
28
|
| Complete a task | `/ttt:done` | "done", "mark complete", "finish task" |
|
|
28
29
|
|
|
@@ -39,6 +40,7 @@ ttt show MP-624 # Show issue details
|
|
|
39
40
|
ttt show --user me # My issues
|
|
40
41
|
ttt work-on next # Auto-select highest priority
|
|
41
42
|
ttt work-on MP-624 # Start specific task
|
|
43
|
+
ttt estimate MP-624 6 # Save a 6-hour human estimate
|
|
42
44
|
ttt status # Current in-progress task
|
|
43
45
|
ttt status MP-624 --set +1 # Advance status
|
|
44
46
|
ttt done -m "summary" # Complete with message
|
|
@@ -53,7 +55,7 @@ ttt done -m "summary" # Complete with message
|
|
|
53
55
|
## Standard Workflow
|
|
54
56
|
|
|
55
57
|
```
|
|
56
|
-
ttt sync → ttt work-on next → [implement] → git commit → ttt done -m "..."
|
|
58
|
+
ttt sync → ttt work-on next → ttt estimate <id> <hours> → [implement] → git commit → ttt done -m "..."
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
## File Structure
|
|
@@ -62,7 +64,7 @@ ttt sync → ttt work-on next → [implement] → git commit → ttt done -m "..
|
|
|
62
64
|
.ttt/
|
|
63
65
|
├── config.toon # Team configuration
|
|
64
66
|
├── local.toon # Personal settings
|
|
65
|
-
├── cycle.toon # Current cycle tasks (auto-generated)
|
|
67
|
+
├── cycle.toon # Current cycle tasks + local estimates (auto-generated)
|
|
66
68
|
└── output/ # Downloaded attachments
|
|
67
69
|
```
|
|
68
70
|
|