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 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,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
@@ -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);
@@ -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
- const task = await fetchIssueDetail(issueId, { client });
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);
@@ -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
  }
@@ -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
- console.log(` ${getStatusIcon(t.localStatus)} ${t.id}: ${t.title} [${t.localStatus}]`);
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
- console.error(`Issue ${issueId} not found in local data.`);
85
- process.exit(1);
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}`);
@@ -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 task = {
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 task = {
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
@@ -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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "team-toon-tack",
3
- "version": "3.2.12",
3
+ "version": "3.2.14",
4
4
  "description": "Linear & Trello task sync & management CLI with TOON format",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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