planflow-plugin 0.1.0
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/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/cli.js +169 -0
- package/bin/postinstall.js +87 -0
- package/commands/pfActivity/SKILL.md +725 -0
- package/commands/pfAssign/SKILL.md +623 -0
- package/commands/pfCloudLink/SKILL.md +192 -0
- package/commands/pfCloudList/SKILL.md +222 -0
- package/commands/pfCloudNew/SKILL.md +187 -0
- package/commands/pfCloudUnlink/SKILL.md +152 -0
- package/commands/pfComment/SKILL.md +227 -0
- package/commands/pfComments/SKILL.md +159 -0
- package/commands/pfConnectionStatus/SKILL.md +433 -0
- package/commands/pfDiscord/SKILL.md +740 -0
- package/commands/pfGithubBranch/SKILL.md +672 -0
- package/commands/pfGithubIssue/SKILL.md +963 -0
- package/commands/pfGithubLink/SKILL.md +859 -0
- package/commands/pfGithubPr/SKILL.md +1335 -0
- package/commands/pfGithubUnlink/SKILL.md +401 -0
- package/commands/pfLive/SKILL.md +185 -0
- package/commands/pfLogin/SKILL.md +249 -0
- package/commands/pfLogout/SKILL.md +155 -0
- package/commands/pfMyTasks/SKILL.md +198 -0
- package/commands/pfNotificationSettings/SKILL.md +619 -0
- package/commands/pfNotifications/SKILL.md +420 -0
- package/commands/pfNotificationsClear/SKILL.md +421 -0
- package/commands/pfReact/SKILL.md +232 -0
- package/commands/pfSlack/SKILL.md +659 -0
- package/commands/pfSyncPull/SKILL.md +210 -0
- package/commands/pfSyncPush/SKILL.md +299 -0
- package/commands/pfSyncStatus/SKILL.md +212 -0
- package/commands/pfTeamInvite/SKILL.md +161 -0
- package/commands/pfTeamList/SKILL.md +253 -0
- package/commands/pfTeamRemove/SKILL.md +115 -0
- package/commands/pfTeamRole/SKILL.md +160 -0
- package/commands/pfTestWebhooks/SKILL.md +722 -0
- package/commands/pfUnassign/SKILL.md +134 -0
- package/commands/pfWhoami/SKILL.md +258 -0
- package/commands/pfWorkload/SKILL.md +219 -0
- package/commands/planExportCsv/SKILL.md +106 -0
- package/commands/planExportGithub/SKILL.md +222 -0
- package/commands/planExportJson/SKILL.md +159 -0
- package/commands/planExportSummary/SKILL.md +158 -0
- package/commands/planNew/SKILL.md +641 -0
- package/commands/planNext/SKILL.md +1200 -0
- package/commands/planSettingsAutoSync/SKILL.md +199 -0
- package/commands/planSettingsLanguage/SKILL.md +201 -0
- package/commands/planSettingsReset/SKILL.md +237 -0
- package/commands/planSettingsShow/SKILL.md +482 -0
- package/commands/planSpec/SKILL.md +929 -0
- package/commands/planUpdate/SKILL.md +2518 -0
- package/commands/team/SKILL.md +740 -0
- package/locales/en.json +1499 -0
- package/locales/ka.json +1499 -0
- package/package.json +48 -0
- package/templates/PROJECT_PLAN.template.md +157 -0
- package/templates/backend-api.template.md +562 -0
- package/templates/frontend-spa.template.md +610 -0
- package/templates/fullstack.template.md +397 -0
- package/templates/ka/backend-api.template.md +562 -0
- package/templates/ka/frontend-spa.template.md +610 -0
- package/templates/ka/fullstack.template.md +397 -0
- package/templates/sections/architecture.md +21 -0
- package/templates/sections/overview.md +15 -0
- package/templates/sections/tasks.md +22 -0
- package/templates/sections/tech-stack.md +19 -0
|
@@ -0,0 +1,2518 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planUpdate
|
|
3
|
+
description: Plan Update Command
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Plan Update Command
|
|
7
|
+
|
|
8
|
+
You are a task progress tracking assistant. Your role is to update task statuses in PROJECT_PLAN.md and recalculate progress metrics.
|
|
9
|
+
|
|
10
|
+
## ⚠️ IMPORTANT: Auto-Sync Requirement (v1.2.0+)
|
|
11
|
+
|
|
12
|
+
**After updating the local file (Step 7), you MUST always execute Step 8 (Cloud Integration) to check if auto-sync is enabled and sync to cloud if conditions are met. This is NOT optional!**
|
|
13
|
+
|
|
14
|
+
## Objective
|
|
15
|
+
|
|
16
|
+
Update the status of tasks in PROJECT_PLAN.md, recalculate progress percentages, and maintain accurate project tracking.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
/planUpdate <task-id> <action> [--force]
|
|
22
|
+
/planUpdate T1.1 start # Mark task as in progress
|
|
23
|
+
/planUpdate T1.1 done # Mark task as completed
|
|
24
|
+
/planUpdate T2.3 block # Mark task as blocked
|
|
25
|
+
/planUpdate T2.1 done --force # Update even if assigned to someone else
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Flags:**
|
|
29
|
+
- `--force` - Skip assignment check and update task regardless of who it's assigned to
|
|
30
|
+
|
|
31
|
+
## Process
|
|
32
|
+
|
|
33
|
+
### Step 0: Load User Language & Translations
|
|
34
|
+
|
|
35
|
+
**CRITICAL: Execute this step FIRST, before any output!**
|
|
36
|
+
|
|
37
|
+
Load user's language preference using hierarchical config (local → global → default) and translation file.
|
|
38
|
+
|
|
39
|
+
**Pseudo-code:**
|
|
40
|
+
```javascript
|
|
41
|
+
// Read config with hierarchy AND MERGE (v1.2.0+)
|
|
42
|
+
function getMergedConfig() {
|
|
43
|
+
let globalConfig = {}
|
|
44
|
+
let localConfig = {}
|
|
45
|
+
|
|
46
|
+
// Read global config first (base)
|
|
47
|
+
const globalPath = expandPath("~/.config/claude/plan-plugin-config.json")
|
|
48
|
+
if (fileExists(globalPath)) {
|
|
49
|
+
try {
|
|
50
|
+
globalConfig = JSON.parse(readFile(globalPath))
|
|
51
|
+
} catch (error) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Read local config (overrides)
|
|
55
|
+
if (fileExists("./.plan-config.json")) {
|
|
56
|
+
try {
|
|
57
|
+
localConfig = JSON.parse(readFile("./.plan-config.json"))
|
|
58
|
+
} catch (error) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Merge configs: local overrides global, but cloud settings are merged
|
|
62
|
+
const mergedConfig = {
|
|
63
|
+
...globalConfig,
|
|
64
|
+
...localConfig,
|
|
65
|
+
cloud: {
|
|
66
|
+
...(globalConfig.cloud || {}),
|
|
67
|
+
...(localConfig.cloud || {})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return mergedConfig
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const config = getMergedConfig()
|
|
75
|
+
const language = config.language || "en"
|
|
76
|
+
|
|
77
|
+
// Cloud config (v1.2.0+) - now properly merged from both configs
|
|
78
|
+
const cloudConfig = config.cloud || {}
|
|
79
|
+
const isAuthenticated = !!cloudConfig.apiToken
|
|
80
|
+
const apiUrl = cloudConfig.apiUrl || "https://api.planflow.tools"
|
|
81
|
+
const autoSync = cloudConfig.autoSync || false
|
|
82
|
+
const linkedProjectId = cloudConfig.projectId || null
|
|
83
|
+
|
|
84
|
+
// Load translations
|
|
85
|
+
const translationPath = `locales/${language}.json`
|
|
86
|
+
const t = JSON.parse(readFile(translationPath))
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Instructions for Claude:**
|
|
90
|
+
|
|
91
|
+
1. Read BOTH config files and MERGE them:
|
|
92
|
+
- First read `~/.config/claude/plan-plugin-config.json` (global, base)
|
|
93
|
+
- Then read `./.plan-config.json` (local, overrides)
|
|
94
|
+
- Merge the `cloud` sections: global values + local overrides
|
|
95
|
+
2. This ensures:
|
|
96
|
+
- `apiToken` from global config is available
|
|
97
|
+
- `projectId` from global config is available
|
|
98
|
+
- `autoSync` from local config overrides global
|
|
99
|
+
3. Use Read tool: `locales/{language}.json`
|
|
100
|
+
4. Store as `t` variable
|
|
101
|
+
|
|
102
|
+
**Example merge:**
|
|
103
|
+
```javascript
|
|
104
|
+
// Global config:
|
|
105
|
+
{ "cloud": { "apiToken": "pf_xxx", "projectId": "abc123" } }
|
|
106
|
+
|
|
107
|
+
// Local config:
|
|
108
|
+
{ "cloud": { "autoSync": true } }
|
|
109
|
+
|
|
110
|
+
// Merged result:
|
|
111
|
+
{ "cloud": { "apiToken": "pf_xxx", "projectId": "abc123", "autoSync": true } }
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Step 0.5: Show Notification Badge (v1.6.0+)
|
|
115
|
+
|
|
116
|
+
**Purpose:** Display unread notification count to keep users informed of team activity.
|
|
117
|
+
|
|
118
|
+
**When to Execute:** Only if authenticated AND linked to a project.
|
|
119
|
+
|
|
120
|
+
**Bash Implementation:**
|
|
121
|
+
```bash
|
|
122
|
+
API_URL="https://api.planflow.tools"
|
|
123
|
+
TOKEN="$API_TOKEN"
|
|
124
|
+
PROJECT_ID="$PROJECT_ID"
|
|
125
|
+
|
|
126
|
+
# Only proceed if authenticated and linked
|
|
127
|
+
if [ -n "$TOKEN" ] && [ -n "$PROJECT_ID" ]; then
|
|
128
|
+
# Fetch unread count with short timeout
|
|
129
|
+
RESPONSE=$(curl -s --connect-timeout 3 --max-time 5 \
|
|
130
|
+
-X GET \
|
|
131
|
+
-H "Accept: application/json" \
|
|
132
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
133
|
+
"${API_URL}/projects/${PROJECT_ID}/notifications?limit=1&unread=true" 2>/dev/null)
|
|
134
|
+
|
|
135
|
+
if [ $? -eq 0 ]; then
|
|
136
|
+
UNREAD_COUNT=$(echo "$RESPONSE" | grep -o '"unreadCount":[0-9]*' | grep -o '[0-9]*')
|
|
137
|
+
if [ -n "$UNREAD_COUNT" ] && [ "$UNREAD_COUNT" -gt 0 ]; then
|
|
138
|
+
echo "🔔 $UNREAD_COUNT unread notification(s) — /pfNotifications to view"
|
|
139
|
+
echo ""
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Example Output (if 3 unread):**
|
|
146
|
+
```
|
|
147
|
+
🔔 3 unread notifications — /pfNotifications to view
|
|
148
|
+
|
|
149
|
+
[... rest of update output ...]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Instructions for Claude:**
|
|
153
|
+
|
|
154
|
+
1. After loading config and translations (Step 0), check if `cloudConfig.apiToken` AND `cloudConfig.projectId` exist
|
|
155
|
+
2. If yes, make a quick API call to fetch notification count
|
|
156
|
+
3. If unreadCount > 0, display the badge line with a blank line after
|
|
157
|
+
4. If any error occurs (timeout, network, auth), silently skip and continue
|
|
158
|
+
5. Proceed to Step 1 regardless of badge result
|
|
159
|
+
|
|
160
|
+
**Important:** Never let this step block or delay the main command. Use short timeouts and fail silently.
|
|
161
|
+
|
|
162
|
+
See: `skills/notification-badge/SKILL.md` for full implementation details.
|
|
163
|
+
|
|
164
|
+
### Step 1: Validate Inputs
|
|
165
|
+
|
|
166
|
+
Check that the user provided:
|
|
167
|
+
1. Task ID (e.g., T1.1, T2.3)
|
|
168
|
+
2. Action: `start`, `done`, or `block`
|
|
169
|
+
|
|
170
|
+
If missing, show usage:
|
|
171
|
+
```
|
|
172
|
+
{t.commands.update.usage}
|
|
173
|
+
|
|
174
|
+
{t.commands.update.actions}
|
|
175
|
+
{t.commands.update.startAction}
|
|
176
|
+
{t.commands.update.doneAction}
|
|
177
|
+
{t.commands.update.blockAction}
|
|
178
|
+
|
|
179
|
+
{t.commands.update.example}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Example output (English):**
|
|
183
|
+
```
|
|
184
|
+
Usage: /planUpdate <task-id> <action>
|
|
185
|
+
|
|
186
|
+
Actions:
|
|
187
|
+
start - Mark task as in progress (TODO → IN_PROGRESS)
|
|
188
|
+
done - Mark task as completed (ANY → DONE)
|
|
189
|
+
block - Mark task as blocked (ANY → BLOCKED)
|
|
190
|
+
|
|
191
|
+
Example: /planUpdate T1.1 start
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Example output (Georgian):**
|
|
195
|
+
```
|
|
196
|
+
გამოყენება: /planUpdate <task-id> <action>
|
|
197
|
+
|
|
198
|
+
მოქმედებები:
|
|
199
|
+
start - მონიშნე ამოცანა როგორც მიმდინარე (TODO → IN_PROGRESS)
|
|
200
|
+
done - მონიშნე ამოცანა როგორც დასრულებული (ANY → DONE)
|
|
201
|
+
block - მონიშნე ამოცანა როგორც დაბლოკილი (ANY → BLOCKED)
|
|
202
|
+
|
|
203
|
+
მაგალითი: /planUpdate T1.1 start
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Step 2: Read PROJECT_PLAN.md
|
|
207
|
+
|
|
208
|
+
Use the Read tool to read the PROJECT_PLAN.md file from the current working directory.
|
|
209
|
+
|
|
210
|
+
If file doesn't exist, output:
|
|
211
|
+
```
|
|
212
|
+
{t.commands.update.planNotFound}
|
|
213
|
+
|
|
214
|
+
{t.commands.update.runPlanNew}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**Example:**
|
|
218
|
+
- EN: "❌ Error: PROJECT_PLAN.md not found in current directory. Please run /planNew first to create a project plan."
|
|
219
|
+
- KA: "❌ შეცდომა: PROJECT_PLAN.md არ მოიძებნა მიმდინარე დირექტორიაში. გთხოვთ ჯერ გაუშვათ /planNew პროექტის გეგმის შესაქმნელად."
|
|
220
|
+
|
|
221
|
+
### Step 3: Find the Task
|
|
222
|
+
|
|
223
|
+
Search for the task ID in the file. Tasks are formatted as:
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
#### T1.1: Task Name
|
|
227
|
+
- [ ] **Status**: TODO
|
|
228
|
+
- **Complexity**: Low
|
|
229
|
+
- **Estimated**: 2 hours
|
|
230
|
+
...
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
or
|
|
234
|
+
|
|
235
|
+
```markdown
|
|
236
|
+
#### T1.1: Task Name
|
|
237
|
+
- [x] **Status**: DONE ✅
|
|
238
|
+
- **Complexity**: Low
|
|
239
|
+
...
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
If task not found:
|
|
243
|
+
```
|
|
244
|
+
{t.commands.update.taskNotFound.replace("{taskId}", taskId)}
|
|
245
|
+
|
|
246
|
+
{t.commands.update.availableTasks}
|
|
247
|
+
[List first 5-10 task IDs found in the file]
|
|
248
|
+
|
|
249
|
+
{t.commands.update.checkTasksSection}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Example output (English):**
|
|
253
|
+
```
|
|
254
|
+
❌ Error: Task T1.5 not found in PROJECT_PLAN.md
|
|
255
|
+
|
|
256
|
+
Available tasks:
|
|
257
|
+
T1.1, T1.2, T1.3, T1.4, T2.1, T2.2...
|
|
258
|
+
|
|
259
|
+
Tip: Check the "Tasks & Implementation Plan" section for valid task IDs.
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Example output (Georgian):**
|
|
263
|
+
```
|
|
264
|
+
❌ შეცდომა: ამოცანა T1.5 ვერ მოიძებნა PROJECT_PLAN.md-ში
|
|
265
|
+
|
|
266
|
+
ხელმისაწვდომი ამოცანები:
|
|
267
|
+
T1.1, T1.2, T1.3, T1.4, T2.1, T2.2...
|
|
268
|
+
|
|
269
|
+
რჩევა: შეამოწმეთ "ამოცანები და იმპლემენტაციის გეგმა" სექცია ვალიდური task ID-ებისთვის.
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Step 3.5: Check Task Assignment (v1.6.0+)
|
|
273
|
+
|
|
274
|
+
**Purpose:** Before allowing a status update, check if the task is assigned to someone else and warn the user.
|
|
275
|
+
|
|
276
|
+
**When to Execute:**
|
|
277
|
+
- Only when authenticated (`apiToken` exists)
|
|
278
|
+
- Only when linked to a cloud project (`projectId` exists)
|
|
279
|
+
- Skip if `--force` flag is provided
|
|
280
|
+
|
|
281
|
+
**Pseudo-code:**
|
|
282
|
+
```javascript
|
|
283
|
+
async function checkTaskAssignment(taskId, config, forceFlag, t) {
|
|
284
|
+
const cloudConfig = config.cloud || {}
|
|
285
|
+
const isAuthenticated = !!cloudConfig.apiToken
|
|
286
|
+
const projectId = cloudConfig.projectId
|
|
287
|
+
const currentUserEmail = cloudConfig.userEmail
|
|
288
|
+
|
|
289
|
+
// Skip check if not authenticated or not linked
|
|
290
|
+
if (!isAuthenticated || !projectId) {
|
|
291
|
+
return { proceed: true, reason: "not_cloud_enabled" }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Skip check if --force flag is provided
|
|
295
|
+
if (forceFlag) {
|
|
296
|
+
return { proceed: true, reason: "force_flag" }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Fetch task from cloud to get assignment info
|
|
300
|
+
const apiUrl = cloudConfig.apiUrl || "https://api.planflow.tools"
|
|
301
|
+
const response = await fetch(
|
|
302
|
+
`${apiUrl}/projects/${projectId}/tasks/${taskId}`,
|
|
303
|
+
{
|
|
304
|
+
method: "GET",
|
|
305
|
+
headers: {
|
|
306
|
+
"Authorization": `Bearer ${cloudConfig.apiToken}`,
|
|
307
|
+
"Accept": "application/json"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// If task not found on cloud, proceed (local-only task)
|
|
313
|
+
if (response.status === 404) {
|
|
314
|
+
return { proceed: true, reason: "task_not_on_cloud" }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If request failed, proceed with warning
|
|
318
|
+
if (!response.ok) {
|
|
319
|
+
console.log(t.commands.update.assignmentCheckFailed || "⚠️ Could not check task assignment")
|
|
320
|
+
return { proceed: true, reason: "api_error" }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const task = response.data.task
|
|
324
|
+
const assignee = task.assignee
|
|
325
|
+
|
|
326
|
+
// Case 1: Task is not assigned - proceed freely
|
|
327
|
+
if (!assignee) {
|
|
328
|
+
return { proceed: true, reason: "unassigned" }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Case 2: Task is assigned to current user - proceed with positive message
|
|
332
|
+
if (assignee.email === currentUserEmail) {
|
|
333
|
+
return { proceed: true, reason: "assigned_to_me", assignee }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Case 3: Task is assigned to someone else - warn and ask for confirmation
|
|
337
|
+
return {
|
|
338
|
+
proceed: false,
|
|
339
|
+
reason: "assigned_to_other",
|
|
340
|
+
assignee,
|
|
341
|
+
message: t.commands.update.assignedToOther
|
|
342
|
+
.replace("{name}", assignee.name || assignee.email)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Bash Implementation:**
|
|
348
|
+
```bash
|
|
349
|
+
API_URL="https://api.planflow.tools"
|
|
350
|
+
TOKEN="$API_TOKEN"
|
|
351
|
+
PROJECT_ID="$PROJECT_ID"
|
|
352
|
+
TASK_ID="T2.1"
|
|
353
|
+
CURRENT_USER_EMAIL="$USER_EMAIL"
|
|
354
|
+
|
|
355
|
+
# Fetch task to check assignment
|
|
356
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
357
|
+
--connect-timeout 5 \
|
|
358
|
+
--max-time 10 \
|
|
359
|
+
-X GET \
|
|
360
|
+
-H "Accept: application/json" \
|
|
361
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
362
|
+
"${API_URL}/projects/${PROJECT_ID}/tasks/${TASK_ID}")
|
|
363
|
+
|
|
364
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
365
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
366
|
+
|
|
367
|
+
if [ "$HTTP_CODE" -eq 404 ]; then
|
|
368
|
+
# Task not on cloud - proceed
|
|
369
|
+
echo "Task not found on cloud, proceeding with local update"
|
|
370
|
+
elif [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
371
|
+
# Parse assignee
|
|
372
|
+
ASSIGNEE_EMAIL=$(echo "$BODY" | grep -o '"assignee":{[^}]*"email":"[^"]*"' | grep -o '"email":"[^"]*"' | cut -d'"' -f4)
|
|
373
|
+
ASSIGNEE_NAME=$(echo "$BODY" | grep -o '"assignee":{[^}]*"name":"[^"]*"' | grep -o '"name":"[^"]*"' | cut -d'"' -f4)
|
|
374
|
+
|
|
375
|
+
if [ -z "$ASSIGNEE_EMAIL" ]; then
|
|
376
|
+
echo "Task is unassigned, proceeding"
|
|
377
|
+
elif [ "$ASSIGNEE_EMAIL" = "$CURRENT_USER_EMAIL" ]; then
|
|
378
|
+
echo "Task is assigned to you, proceeding"
|
|
379
|
+
else
|
|
380
|
+
echo "⚠️ Task is assigned to: $ASSIGNEE_NAME ($ASSIGNEE_EMAIL)"
|
|
381
|
+
echo "Use --force to update anyway, or /pfUnassign $TASK_ID first"
|
|
382
|
+
fi
|
|
383
|
+
fi
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Flow:**
|
|
387
|
+
|
|
388
|
+
```
|
|
389
|
+
/planUpdate T2.1 done
|
|
390
|
+
│
|
|
391
|
+
▼
|
|
392
|
+
┌─────────────────────────────────────┐
|
|
393
|
+
│ Is --force flag provided? │
|
|
394
|
+
└──────────────┬──────────────────────┘
|
|
395
|
+
│
|
|
396
|
+
┌──────┴──────┐
|
|
397
|
+
│ Yes │ No
|
|
398
|
+
▼ ▼
|
|
399
|
+
Proceed to ┌─────────────────────────────────────┐
|
|
400
|
+
Step 4 │ Is user authenticated + linked? │
|
|
401
|
+
└──────────────┬──────────────────────┘
|
|
402
|
+
│
|
|
403
|
+
┌──────┴──────┐
|
|
404
|
+
│ No │ Yes
|
|
405
|
+
▼ ▼
|
|
406
|
+
Proceed to ┌─────────────────────┐
|
|
407
|
+
Step 4 │ Fetch task from API │
|
|
408
|
+
└──────────┬──────────┘
|
|
409
|
+
│
|
|
410
|
+
▼
|
|
411
|
+
┌─────────────────────┐
|
|
412
|
+
│ Check assignee │
|
|
413
|
+
└──────────┬──────────┘
|
|
414
|
+
│
|
|
415
|
+
┌──────────────┼──────────────┐
|
|
416
|
+
│ │ │
|
|
417
|
+
Unassigned Assigned to me Assigned to other
|
|
418
|
+
│ │ │
|
|
419
|
+
▼ ▼ ▼
|
|
420
|
+
Proceed Proceed + Show warning
|
|
421
|
+
to Step 4 positive msg Ask to confirm
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
**Output Examples:**
|
|
425
|
+
|
|
426
|
+
#### Case 1: Task Assigned to Current User (Positive)
|
|
427
|
+
```
|
|
428
|
+
👤 This task is assigned to you - ready to work on!
|
|
429
|
+
|
|
430
|
+
[Proceeds to Step 4]
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
#### Case 2: Task Assigned to Someone Else (Warning)
|
|
434
|
+
```
|
|
435
|
+
⚠️ Task T2.1 is assigned to Jane Smith (jane@company.com)
|
|
436
|
+
|
|
437
|
+
This task belongs to another team member. Updating it may cause confusion.
|
|
438
|
+
|
|
439
|
+
Options:
|
|
440
|
+
1. Use --force to update anyway: /planUpdate T2.1 done --force
|
|
441
|
+
2. Unassign first: /pfUnassign T2.1
|
|
442
|
+
3. Ask them to update it
|
|
443
|
+
|
|
444
|
+
💡 Tip: Check /pfWorkload to see team task distribution
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**Example output (Georgian):**
|
|
448
|
+
```
|
|
449
|
+
⚠️ ამოცანა T2.1 მინიჭებულია Jane Smith-ზე (jane@company.com)
|
|
450
|
+
|
|
451
|
+
ეს ამოცანა ეკუთვნის გუნდის სხვა წევრს. მისი განახლება შეიძლება გამოიწვიოს დაბნეულობა.
|
|
452
|
+
|
|
453
|
+
ვარიანტები:
|
|
454
|
+
1. გამოიყენე --force მაინც განსაახლებლად: /planUpdate T2.1 done --force
|
|
455
|
+
2. ჯერ მოხსენი მინიჭება: /pfUnassign T2.1
|
|
456
|
+
3. სთხოვე მათ განაახლონ
|
|
457
|
+
|
|
458
|
+
💡 რჩევა: შეამოწმე /pfWorkload გუნდის ამოცანების განაწილების სანახავად
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Case 3: Task Unassigned (Silent Proceed)
|
|
462
|
+
No message shown, proceeds directly to Step 4.
|
|
463
|
+
|
|
464
|
+
#### Case 4: Force Flag Used
|
|
465
|
+
```
|
|
466
|
+
⚡ Force flag detected - skipping assignment check
|
|
467
|
+
|
|
468
|
+
[Proceeds to Step 4]
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Translation Keys Required:**
|
|
472
|
+
```json
|
|
473
|
+
{
|
|
474
|
+
"commands": {
|
|
475
|
+
"update": {
|
|
476
|
+
"assignedToYou": "👤 This task is assigned to you - ready to work on!",
|
|
477
|
+
"assignedToOther": "⚠️ Task {taskId} is assigned to {name}",
|
|
478
|
+
"assignedToOtherEmail": "({email})",
|
|
479
|
+
"assignedWarning": "This task belongs to another team member. Updating it may cause confusion.",
|
|
480
|
+
"assignedOptions": "Options:",
|
|
481
|
+
"assignedForceHint": "1. Use --force to update anyway: /planUpdate {taskId} {action} --force",
|
|
482
|
+
"assignedUnassignHint": "2. Unassign first: /pfUnassign {taskId}",
|
|
483
|
+
"assignedAskHint": "3. Ask them to update it",
|
|
484
|
+
"assignedWorkloadTip": "💡 Tip: Check /pfWorkload to see team task distribution",
|
|
485
|
+
"assignmentCheckFailed": "⚠️ Could not check task assignment",
|
|
486
|
+
"forceSkipping": "⚡ Force flag detected - skipping assignment check"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
**Instructions for Claude:**
|
|
493
|
+
|
|
494
|
+
1. After Step 3 (task found), check if `--force` flag was provided in arguments
|
|
495
|
+
2. If no `--force` flag:
|
|
496
|
+
- Check if user is authenticated and project is linked
|
|
497
|
+
- If yes, make GET request to `/projects/{projectId}/tasks/{taskId}`
|
|
498
|
+
- Parse the assignee from response
|
|
499
|
+
- Compare assignee email with `config.cloud.userEmail`
|
|
500
|
+
3. Based on comparison:
|
|
501
|
+
- **Unassigned**: Proceed silently to Step 4
|
|
502
|
+
- **Assigned to current user**: Show positive message, proceed to Step 4
|
|
503
|
+
- **Assigned to someone else**: Show warning with options, STOP (do not proceed)
|
|
504
|
+
4. If `--force` flag: Skip all checks, proceed to Step 4
|
|
505
|
+
|
|
506
|
+
**Error Handling:**
|
|
507
|
+
- API timeout/error: Show warning but proceed (fail-open for better UX)
|
|
508
|
+
- Task not found on cloud (404): Proceed (local-only task)
|
|
509
|
+
- Network unavailable: Proceed with warning
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### Step 4: Update Task Status
|
|
514
|
+
|
|
515
|
+
Based on the action, update:
|
|
516
|
+
|
|
517
|
+
#### For `start` action:
|
|
518
|
+
- Change checkbox: `- [ ]` → `- [ ]` (stays empty)
|
|
519
|
+
- Change status: `**Status**: TODO` → `**Status**: IN_PROGRESS 🔄`
|
|
520
|
+
|
|
521
|
+
#### For `done` action:
|
|
522
|
+
- Change checkbox: `- [ ]` → `- [x]`
|
|
523
|
+
- Change status: `**Status**: [ANY]` → `**Status**: DONE ✅`
|
|
524
|
+
|
|
525
|
+
#### For `block` action:
|
|
526
|
+
- Change checkbox: `- [ ]` → `- [ ]` (stays empty)
|
|
527
|
+
- Change status: `**Status**: [ANY]` → `**Status**: BLOCKED 🚫`
|
|
528
|
+
|
|
529
|
+
Use the Edit tool to make these changes.
|
|
530
|
+
|
|
531
|
+
### Step 5: Update Progress Tracking
|
|
532
|
+
|
|
533
|
+
Find the "Progress Tracking" section and update:
|
|
534
|
+
|
|
535
|
+
#### Count Tasks
|
|
536
|
+
|
|
537
|
+
Parse all tasks and count:
|
|
538
|
+
- Total tasks: Count all `#### T` task headers
|
|
539
|
+
- Completed tasks: Count all `- [x]` checkboxes
|
|
540
|
+
- In progress tasks: Count all `IN_PROGRESS` statuses
|
|
541
|
+
- Blocked tasks: Count all `BLOCKED` statuses
|
|
542
|
+
|
|
543
|
+
#### Calculate Progress
|
|
544
|
+
|
|
545
|
+
```
|
|
546
|
+
Progress % = (Completed / Total) × 100
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Round to nearest integer.
|
|
550
|
+
|
|
551
|
+
#### Generate Progress Bar
|
|
552
|
+
|
|
553
|
+
Create visual progress bar (10 blocks):
|
|
554
|
+
```
|
|
555
|
+
Completed: 0% → ⬜⬜⬜⬜⬜⬜⬜⬜⬜⬜
|
|
556
|
+
Completed: 15% → 🟩⬜⬜⬜⬜⬜⬜⬜⬜⬜
|
|
557
|
+
Completed: 35% → 🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜
|
|
558
|
+
Completed: 50% → 🟩🟩🟩🟩🟩⬜⬜⬜⬜⬜
|
|
559
|
+
Completed: 75% → 🟩🟩🟩🟩🟩🟩🟩⬜⬜⬜
|
|
560
|
+
Completed: 100% → 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Formula: `filled_blocks = Math.floor(progress_percent / 10)`
|
|
564
|
+
|
|
565
|
+
#### Update Progress Section
|
|
566
|
+
|
|
567
|
+
Find and replace the progress section:
|
|
568
|
+
|
|
569
|
+
```markdown
|
|
570
|
+
### Overall Status
|
|
571
|
+
**Total Tasks**: [X]
|
|
572
|
+
**Completed**: [Y] [PROGRESS_BAR] ([Z]%)
|
|
573
|
+
**In Progress**: [A]
|
|
574
|
+
**Blocked**: [B]
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
#### Update Phase Progress
|
|
578
|
+
|
|
579
|
+
For each phase (Phase 1, Phase 2, etc.):
|
|
580
|
+
1. Count tasks in that phase (T1.X belongs to Phase 1, T2.X to Phase 2, etc.)
|
|
581
|
+
2. Count completed tasks in that phase
|
|
582
|
+
3. Calculate phase percentage
|
|
583
|
+
|
|
584
|
+
Update the phase progress section:
|
|
585
|
+
```markdown
|
|
586
|
+
### Phase Progress
|
|
587
|
+
- 🟢 Phase 1: Foundation → [X]/[Y] ([Z]%)
|
|
588
|
+
- 🔵 Phase 2: Core Features → [A]/[B] ([C]%)
|
|
589
|
+
- 🟣 Phase 3: Advanced Features → [D]/[E] ([F]%)
|
|
590
|
+
- 🟠 Phase 4: Testing & Deployment → [G]/[H] ([I]%)
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
#### Update Current Focus
|
|
594
|
+
|
|
595
|
+
Find the next TODO or IN_PROGRESS task and update:
|
|
596
|
+
|
|
597
|
+
```markdown
|
|
598
|
+
### Current Focus
|
|
599
|
+
🎯 **Next Task**: T[X].[Y] - [Task Name]
|
|
600
|
+
📅 **Phase**: [N] - [Phase Name]
|
|
601
|
+
🔄 **Status**: [Current overall status]
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
#### Update Last Modified Date
|
|
605
|
+
|
|
606
|
+
Find and update the "Last Updated" date at the top of the file:
|
|
607
|
+
|
|
608
|
+
```markdown
|
|
609
|
+
*Last Updated: 2026-01-26*
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Use current date in YYYY-MM-DD format.
|
|
613
|
+
|
|
614
|
+
### Step 6: Save Changes
|
|
615
|
+
|
|
616
|
+
Use the Edit tool to apply all changes to PROJECT_PLAN.md.
|
|
617
|
+
|
|
618
|
+
### Step 7: Auto-Sync and Show Confirmation
|
|
619
|
+
|
|
620
|
+
**This step has TWO parts that MUST both be executed:**
|
|
621
|
+
|
|
622
|
+
#### Part A: Auto-Sync to Cloud (if enabled)
|
|
623
|
+
|
|
624
|
+
Before showing the confirmation message, check if auto-sync should be triggered:
|
|
625
|
+
|
|
626
|
+
```javascript
|
|
627
|
+
// Get merged config (global + local)
|
|
628
|
+
const cloudConfig = config.cloud || {}
|
|
629
|
+
const apiToken = cloudConfig.apiToken
|
|
630
|
+
const projectId = cloudConfig.projectId
|
|
631
|
+
const autoSync = cloudConfig.autoSync === true
|
|
632
|
+
|
|
633
|
+
if (apiToken && projectId && autoSync) {
|
|
634
|
+
// MUST execute auto-sync
|
|
635
|
+
console.log("☁️ Auto-syncing to cloud...")
|
|
636
|
+
|
|
637
|
+
// Map action to status
|
|
638
|
+
const statusMap = { "start": "IN_PROGRESS", "done": "DONE", "block": "BLOCKED" }
|
|
639
|
+
const newStatus = statusMap[action]
|
|
640
|
+
|
|
641
|
+
// Make PATCH request
|
|
642
|
+
const response = await fetch(
|
|
643
|
+
`https://api.planflow.tools/projects/${projectId}/tasks/${taskId}`,
|
|
644
|
+
{
|
|
645
|
+
method: "PATCH",
|
|
646
|
+
headers: {
|
|
647
|
+
"Content-Type": "application/json",
|
|
648
|
+
"Authorization": `Bearer ${apiToken}`
|
|
649
|
+
},
|
|
650
|
+
body: JSON.stringify({ status: newStatus })
|
|
651
|
+
}
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if (response.ok) {
|
|
655
|
+
console.log("☁️ ✅ Synced to cloud")
|
|
656
|
+
} else {
|
|
657
|
+
console.log("☁️ ⚠️ Cloud sync failed (local update succeeded)")
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Claude MUST execute this Bash command if autoSync is enabled:**
|
|
663
|
+
```bash
|
|
664
|
+
curl -s -X PATCH \
|
|
665
|
+
-H "Content-Type: application/json" \
|
|
666
|
+
-H "Authorization: Bearer {API_TOKEN}" \
|
|
667
|
+
-d '{"status": "{NEW_STATUS}"}' \
|
|
668
|
+
"https://api.planflow.tools/projects/{PROJECT_ID}/tasks/{TASK_ID}"
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
Replace:
|
|
672
|
+
- `{API_TOKEN}` with token from merged config
|
|
673
|
+
- `{PROJECT_ID}` with projectId from merged config
|
|
674
|
+
- `{TASK_ID}` with the task ID (e.g., T1.4)
|
|
675
|
+
- `{NEW_STATUS}` with: IN_PROGRESS (start), DONE (done), or BLOCKED (block)
|
|
676
|
+
|
|
677
|
+
#### Part A.5: Broadcast Presence (v1.6.0+ - T12.2)
|
|
678
|
+
|
|
679
|
+
**Purpose:** Broadcast "Currently Working On" status to team members in real-time.
|
|
680
|
+
|
|
681
|
+
**When to Execute:** After cloud sync succeeds, if WebSocket is connected.
|
|
682
|
+
|
|
683
|
+
**Pseudo-code:**
|
|
684
|
+
```javascript
|
|
685
|
+
// Check WebSocket connection status
|
|
686
|
+
const wsState = ws_status() // from skills/websocket/SKILL.md
|
|
687
|
+
|
|
688
|
+
if (wsState === "connected" || wsState === "polling") {
|
|
689
|
+
if (action === "start") {
|
|
690
|
+
// Set presence: "Working on T2.1"
|
|
691
|
+
ws_update_presence(taskId, taskName)
|
|
692
|
+
console.log(t.commands.update.presenceBroadcasted
|
|
693
|
+
.replace("{taskId}", taskId)
|
|
694
|
+
.replace("{action}", "started"))
|
|
695
|
+
} else if (action === "done" || action === "block") {
|
|
696
|
+
// Clear presence: no longer working on this task
|
|
697
|
+
ws_update_presence("")
|
|
698
|
+
console.log(t.commands.update.presenceBroadcasted
|
|
699
|
+
.replace("{taskId}", taskId)
|
|
700
|
+
.replace("{action}", action === "done" ? "completed" : "blocked"))
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
**Bash Implementation:**
|
|
706
|
+
```bash
|
|
707
|
+
# Check WebSocket state
|
|
708
|
+
STATE_FILE="${HOME}/.planflow-ws-state.json"
|
|
709
|
+
WS_STATE="disconnected"
|
|
710
|
+
|
|
711
|
+
if [ -f "$STATE_FILE" ]; then
|
|
712
|
+
WS_STATE=$(jq -r '.state // "disconnected"' "$STATE_FILE")
|
|
713
|
+
fi
|
|
714
|
+
|
|
715
|
+
# Broadcast presence if connected
|
|
716
|
+
if [ "$WS_STATE" = "connected" ] || [ "$WS_STATE" = "polling" ]; then
|
|
717
|
+
if [ "$ACTION" = "start" ]; then
|
|
718
|
+
# Set "Working on" presence
|
|
719
|
+
PRESENCE_MSG=$(jq -n \
|
|
720
|
+
--arg taskId "$TASK_ID" \
|
|
721
|
+
--arg taskName "$TASK_NAME" \
|
|
722
|
+
'{
|
|
723
|
+
type: "presence",
|
|
724
|
+
status: "working",
|
|
725
|
+
taskId: $taskId,
|
|
726
|
+
taskName: $taskName
|
|
727
|
+
}')
|
|
728
|
+
|
|
729
|
+
# Update local state file
|
|
730
|
+
jq --arg taskId "$TASK_ID" --arg taskName "$TASK_NAME" \
|
|
731
|
+
'.presence = { taskId: $taskId, taskName: $taskName, since: (now | todate) }' \
|
|
732
|
+
"$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
733
|
+
|
|
734
|
+
echo "🟢 Broadcasting: Working on $TASK_ID to team members"
|
|
735
|
+
else
|
|
736
|
+
# Clear presence for "done" or "block"
|
|
737
|
+
PRESENCE_MSG='{"type":"presence","status":"idle","taskId":null}'
|
|
738
|
+
|
|
739
|
+
# Clear local state
|
|
740
|
+
if [ -f "$STATE_FILE" ]; then
|
|
741
|
+
jq '.presence = null' "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
|
|
742
|
+
fi
|
|
743
|
+
|
|
744
|
+
if [ "$ACTION" = "done" ]; then
|
|
745
|
+
echo "🟢 Broadcasting: $TASK_ID completed to team members"
|
|
746
|
+
else
|
|
747
|
+
echo "🟢 Broadcasting: $TASK_ID blocked to team members"
|
|
748
|
+
fi
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
# Send via WebSocket (non-blocking)
|
|
752
|
+
# The ws_send function handles queueing if offline
|
|
753
|
+
ws_send "$PRESENCE_MSG" 2>/dev/null &
|
|
754
|
+
fi
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**Output Examples:**
|
|
758
|
+
|
|
759
|
+
For `/planUpdate T2.1 start`:
|
|
760
|
+
```
|
|
761
|
+
☁️ ✅ Synced to cloud
|
|
762
|
+
🟢 Broadcasting: Working on T2.1 to team members
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
For `/planUpdate T2.1 done`:
|
|
766
|
+
```
|
|
767
|
+
☁️ ✅ Synced to cloud
|
|
768
|
+
🟢 Broadcasting: T2.1 completed to team members
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
For `/planUpdate T2.1 block`:
|
|
772
|
+
```
|
|
773
|
+
☁️ ✅ Synced to cloud
|
|
774
|
+
🟢 Broadcasting: T2.1 blocked to team members
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
**When Offline/Disconnected:**
|
|
778
|
+
If WebSocket is disconnected, presence messages are automatically queued via the offline queue system. They'll be sent when reconnected.
|
|
779
|
+
|
|
780
|
+
```
|
|
781
|
+
☁️ ✅ Synced to cloud
|
|
782
|
+
📤 Presence update queued (will broadcast when connected)
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
**Translation Keys:**
|
|
786
|
+
```json
|
|
787
|
+
{
|
|
788
|
+
"commands": {
|
|
789
|
+
"update": {
|
|
790
|
+
"presenceBroadcasting": "🔄 Broadcasting status to team...",
|
|
791
|
+
"presenceBroadcasted": "🟢 Broadcasting: {action} {taskId} to team members",
|
|
792
|
+
"presenceWorkingOn": "🟢 Broadcasting: Working on {taskId} to team members",
|
|
793
|
+
"presenceCompleted": "🟢 Broadcasting: {taskId} completed to team members",
|
|
794
|
+
"presenceBlocked": "🟢 Broadcasting: {taskId} blocked to team members",
|
|
795
|
+
"presenceQueued": "📤 Presence update queued (will broadcast when connected)",
|
|
796
|
+
"presenceFailed": "⚠️ Could not broadcast presence (local update succeeded)"
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
**Team Members See:**
|
|
803
|
+
When you start a task, other team members running `/pfTeamList` or `/team` will see:
|
|
804
|
+
```
|
|
805
|
+
👥 Team Members
|
|
806
|
+
|
|
807
|
+
🟢 John Doe (Editor) john@company.com
|
|
808
|
+
Working on: T2.1 - Implement login API
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Instructions for Claude:**
|
|
812
|
+
|
|
813
|
+
1. After successful cloud sync (Part A), check WebSocket state file
|
|
814
|
+
2. If connected or polling:
|
|
815
|
+
- For "start": send presence with taskId and taskName
|
|
816
|
+
- For "done"/"block": send idle presence to clear status
|
|
817
|
+
3. Update local state file with current presence
|
|
818
|
+
4. Show broadcasting confirmation in output
|
|
819
|
+
5. If disconnected: queue message, show queued indicator
|
|
820
|
+
6. Never let presence failure block the update flow
|
|
821
|
+
|
|
822
|
+
#### Part B: Show Confirmation
|
|
823
|
+
|
|
824
|
+
Display a success message with updated metrics using translations.
|
|
825
|
+
|
|
826
|
+
**Pseudo-code:**
|
|
827
|
+
```javascript
|
|
828
|
+
const action = userAction // "start", "done", or "block"
|
|
829
|
+
let statusMessage
|
|
830
|
+
|
|
831
|
+
if (action === "start") {
|
|
832
|
+
statusMessage = t.commands.update.taskStarted.replace("{taskId}", taskId)
|
|
833
|
+
} else if (action === "done") {
|
|
834
|
+
statusMessage = t.commands.update.taskCompleted.replace("{taskId}", taskId)
|
|
835
|
+
} else if (action === "block") {
|
|
836
|
+
statusMessage = t.commands.update.taskBlocked.replace("{taskId}", taskId)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
let output = statusMessage + "\n\n"
|
|
840
|
+
|
|
841
|
+
// Progress update
|
|
842
|
+
const progressDelta = newProgress - oldProgress
|
|
843
|
+
output += t.commands.update.progressUpdate
|
|
844
|
+
.replace("{old}", oldProgress)
|
|
845
|
+
.replace("{new}", newProgress)
|
|
846
|
+
.replace("{delta}", progressDelta) + "\n\n"
|
|
847
|
+
|
|
848
|
+
// Overall status
|
|
849
|
+
output += t.commands.update.overallStatus + "\n"
|
|
850
|
+
output += t.commands.update.total + " " + totalTasks + "\n"
|
|
851
|
+
output += t.commands.update.done + " " + doneTasks + "\n"
|
|
852
|
+
output += t.commands.update.inProgress + " " + inProgressTasks + "\n"
|
|
853
|
+
output += t.commands.update.blocked + " " + blockedTasks + "\n"
|
|
854
|
+
output += t.commands.update.remaining + " " + remainingTasks + "\n\n"
|
|
855
|
+
output += progressBar + " " + newProgress + "%\n\n"
|
|
856
|
+
output += t.commands.update.nextSuggestion
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
**Example output (English):**
|
|
860
|
+
|
|
861
|
+
```
|
|
862
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
863
|
+
│ ✅ Task Completed │
|
|
864
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
865
|
+
│ │
|
|
866
|
+
│ Task T1.2 completed! 🎉 │
|
|
867
|
+
│ │
|
|
868
|
+
│ ── Progress ────────────────────────────────────────────────────────────── │
|
|
869
|
+
│ │
|
|
870
|
+
│ 📊 25% → 31% (+6%) │
|
|
871
|
+
│ │
|
|
872
|
+
│ 🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜ 31% │
|
|
873
|
+
│ │
|
|
874
|
+
│ ── Overall Status ──────────────────────────────────────────────────────── │
|
|
875
|
+
│ │
|
|
876
|
+
│ 📋 Total: 18 │
|
|
877
|
+
│ ✅ Done: 6 │
|
|
878
|
+
│ 🔄 In Progress: 1 │
|
|
879
|
+
│ 🚫 Blocked: 0 │
|
|
880
|
+
│ 📋 Remaining: 11 │
|
|
881
|
+
│ │
|
|
882
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
883
|
+
│ │
|
|
884
|
+
│ 💡 {t.ui.labels.nextSteps} │
|
|
885
|
+
│ • /planNext Get next task recommendation │
|
|
886
|
+
│ │
|
|
887
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
888
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Example output (Georgian):**
|
|
892
|
+
|
|
893
|
+
```
|
|
894
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
895
|
+
│ ✅ ამოცანა დასრულდა │
|
|
896
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
897
|
+
│ │
|
|
898
|
+
│ ამოცანა T1.2 დასრულდა! 🎉 │
|
|
899
|
+
│ │
|
|
900
|
+
│ ── პროგრესი ────────────────────────────────────────────────────────────── │
|
|
901
|
+
│ │
|
|
902
|
+
│ 📊 25% → 31% (+6%) │
|
|
903
|
+
│ │
|
|
904
|
+
│ 🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜ 31% │
|
|
905
|
+
│ │
|
|
906
|
+
│ ── საერთო სტატუსი ──────────────────────────────────────────────────────── │
|
|
907
|
+
│ │
|
|
908
|
+
│ 📋 სულ: 18 │
|
|
909
|
+
│ ✅ დასრულებული: 6 │
|
|
910
|
+
│ 🔄 მიმდინარე: 1 │
|
|
911
|
+
│ 🚫 დაბლოკილი: 0 │
|
|
912
|
+
│ 📋 დარჩენილი: 11 │
|
|
913
|
+
│ │
|
|
914
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
915
|
+
│ │
|
|
916
|
+
│ 💡 შემდეგი ნაბიჯები: │
|
|
917
|
+
│ • /planNext რეკომენდაციის მისაღებად │
|
|
918
|
+
│ │
|
|
919
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
920
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
**Instructions for Claude:**
|
|
924
|
+
|
|
925
|
+
Use translation keys:
|
|
926
|
+
- Task started: `t.commands.update.taskStarted.replace("{taskId}", actualTaskId)`
|
|
927
|
+
- Task completed: `t.commands.update.taskCompleted.replace("{taskId}", actualTaskId)`
|
|
928
|
+
- Task blocked: `t.commands.update.taskBlocked.replace("{taskId}", actualTaskId)`
|
|
929
|
+
- Progress: `t.commands.update.progressUpdate` with {old}, {new}, {delta} replacements
|
|
930
|
+
- Overall status: `t.commands.update.overallStatus`
|
|
931
|
+
- Total: `t.commands.update.total`
|
|
932
|
+
- Done: `t.commands.update.done`
|
|
933
|
+
- In Progress: `t.commands.update.inProgress`
|
|
934
|
+
- Blocked: `t.commands.update.blocked`
|
|
935
|
+
- Remaining: `t.commands.update.remaining`
|
|
936
|
+
- Next suggestion: `t.commands.update.nextSuggestion`
|
|
937
|
+
|
|
938
|
+
**⚠️ IMPORTANT: After showing the confirmation message, you MUST proceed to Step 8 (Cloud Integration) to check for auto-sync!**
|
|
939
|
+
|
|
940
|
+
## Special Cases
|
|
941
|
+
|
|
942
|
+
### Completing Tasks with Dependencies
|
|
943
|
+
|
|
944
|
+
When marking a task as DONE that other tasks depend on, mention it:
|
|
945
|
+
|
|
946
|
+
**Pseudo-code:**
|
|
947
|
+
```javascript
|
|
948
|
+
let output = t.commands.update.taskCompleted.replace("{taskId}", taskId) + "\n\n"
|
|
949
|
+
|
|
950
|
+
if (unlockedTasks.length > 0) {
|
|
951
|
+
output += t.commands.update.unlockedTasks + "\n"
|
|
952
|
+
output += unlockedTasks.map(t => ` - ${t.id}: ${t.name}`).join("\n")
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Example output (English):**
|
|
957
|
+
|
|
958
|
+
```
|
|
959
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
960
|
+
│ ✅ Task Completed │
|
|
961
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
962
|
+
│ │
|
|
963
|
+
│ Task T1.2 completed! 🎉 │
|
|
964
|
+
│ │
|
|
965
|
+
│ ── Unlocked Tasks ──────────────────────────────────────────────────────── │
|
|
966
|
+
│ │
|
|
967
|
+
│ 🔓 T1.3: Database Setup │
|
|
968
|
+
│ 🔓 T2.1: API Endpoints │
|
|
969
|
+
│ │
|
|
970
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
**Example output (Georgian):**
|
|
974
|
+
|
|
975
|
+
```
|
|
976
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
977
|
+
│ ✅ ამოცანა დასრულდა │
|
|
978
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
979
|
+
│ │
|
|
980
|
+
│ ამოცანა T1.2 დასრულდა! 🎉 │
|
|
981
|
+
│ │
|
|
982
|
+
│ ── განბლოკილი ამოცანები ────────────────────────────────────────────────── │
|
|
983
|
+
│ │
|
|
984
|
+
│ 🔓 T1.3: მონაცემთა ბაზის დაყენება │
|
|
985
|
+
│ 🔓 T2.1: API Endpoints │
|
|
986
|
+
│ │
|
|
987
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
To detect this, look for tasks that list the completed task in their "Dependencies" field.
|
|
991
|
+
|
|
992
|
+
**Instructions for Claude:**
|
|
993
|
+
|
|
994
|
+
Use `t.commands.update.unlockedTasks` when showing unlocked tasks.
|
|
995
|
+
|
|
996
|
+
### Blocking a Task
|
|
997
|
+
|
|
998
|
+
When marking a task as BLOCKED, show helpful tip:
|
|
999
|
+
|
|
1000
|
+
**Pseudo-code:**
|
|
1001
|
+
```javascript
|
|
1002
|
+
let output = t.commands.update.taskBlocked.replace("{taskId}", taskId) + "\n\n"
|
|
1003
|
+
output += t.commands.update.tipDocumentBlocker + "\n"
|
|
1004
|
+
output += t.commands.update.whatBlocking + "\n"
|
|
1005
|
+
output += t.commands.update.whatNeeded + "\n"
|
|
1006
|
+
output += t.commands.update.whoCanHelp + "\n\n"
|
|
1007
|
+
output += t.commands.update.considerNewTask
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**Example output (English):**
|
|
1011
|
+
|
|
1012
|
+
```
|
|
1013
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1014
|
+
│ 🚫 Task Blocked │
|
|
1015
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1016
|
+
│ │
|
|
1017
|
+
│ Task T2.3 marked as blocked. │
|
|
1018
|
+
│ │
|
|
1019
|
+
│ ── Document the Blocker ────────────────────────────────────────────────── │
|
|
1020
|
+
│ │
|
|
1021
|
+
│ 💡 Add to task description: │
|
|
1022
|
+
│ • What is blocking this task? │
|
|
1023
|
+
│ • What needs to happen to unblock it? │
|
|
1024
|
+
│ • Who can help resolve this? │
|
|
1025
|
+
│ │
|
|
1026
|
+
│ Consider creating a new task to resolve the blocker. │
|
|
1027
|
+
│ │
|
|
1028
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
**Example output (Georgian):**
|
|
1032
|
+
|
|
1033
|
+
```
|
|
1034
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1035
|
+
│ 🚫 ამოცანა დაბლოკილია │
|
|
1036
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1037
|
+
│ │
|
|
1038
|
+
│ ამოცანა T2.3 მონიშნულია როგორც დაბლოკილი. │
|
|
1039
|
+
│ │
|
|
1040
|
+
│ ── დააფიქსირეთ ბლოკერი ─────────────────────────────────────────────────── │
|
|
1041
|
+
│ │
|
|
1042
|
+
│ 💡 ამოცანის აღწერაში დაამატეთ: │
|
|
1043
|
+
│ • რა აბლოკავს ამ ამოცანას? │
|
|
1044
|
+
│ • რა უნდა მოხდეს მისი განსაბლოკად? │
|
|
1045
|
+
│ • ვინ შეუძლია დაეხმაროს ამის მოგვარებაში? │
|
|
1046
|
+
│ │
|
|
1047
|
+
│ განიხილეთ ახალი ამოცანის შექმნა ბლოკერის მოსაგვარებლად. │
|
|
1048
|
+
│ │
|
|
1049
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
**Instructions for Claude:**
|
|
1053
|
+
|
|
1054
|
+
Use translation keys:
|
|
1055
|
+
- `t.commands.update.taskBlocked`
|
|
1056
|
+
- `t.commands.update.tipDocumentBlocker`
|
|
1057
|
+
- `t.commands.update.whatBlocking`
|
|
1058
|
+
- `t.commands.update.whatNeeded`
|
|
1059
|
+
- `t.commands.update.whoCanHelp`
|
|
1060
|
+
- `t.commands.update.considerNewTask`
|
|
1061
|
+
|
|
1062
|
+
### Completing Final Task
|
|
1063
|
+
|
|
1064
|
+
When the last task is marked as DONE:
|
|
1065
|
+
|
|
1066
|
+
```
|
|
1067
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1068
|
+
│ 🎉 PROJECT COMPLETE │
|
|
1069
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1070
|
+
│ │
|
|
1071
|
+
│ Congratulations! All tasks completed! │
|
|
1072
|
+
│ │
|
|
1073
|
+
│ ── Project Summary ─────────────────────────────────────────────────────── │
|
|
1074
|
+
│ │
|
|
1075
|
+
│ ✅ Project: [PROJECT_NAME] │
|
|
1076
|
+
│ 📊 Progress: ████████████████████████████████ 100% │
|
|
1077
|
+
│ 🏆 Tasks: [Total] completed │
|
|
1078
|
+
│ │
|
|
1079
|
+
│ ╭────────────────────────────────────────────────────────────────────────╮ │
|
|
1080
|
+
│ │ ✅ Project Status: COMPLETE │ │
|
|
1081
|
+
│ ╰────────────────────────────────────────────────────────────────────────╯ │
|
|
1082
|
+
│ │
|
|
1083
|
+
│ Great work on finishing this project! 🚀 │
|
|
1084
|
+
│ │
|
|
1085
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1086
|
+
│ │
|
|
1087
|
+
│ 💡 {t.ui.labels.nextSteps} │
|
|
1088
|
+
│ • Review the project documentation │
|
|
1089
|
+
│ • Deploy to production (if not already done) │
|
|
1090
|
+
│ • Gather user feedback │
|
|
1091
|
+
│ • Plan next phase or features │
|
|
1092
|
+
│ │
|
|
1093
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1094
|
+
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Update the overall status in the Overview section from "In Progress" to "Complete".
|
|
1098
|
+
|
|
1099
|
+
### Invalid State Transitions
|
|
1100
|
+
|
|
1101
|
+
Some transitions don't make sense. Allow all but note:
|
|
1102
|
+
|
|
1103
|
+
```
|
|
1104
|
+
⚠️ Note: Task T1.1 was TODO, now marked BLOCKED.
|
|
1105
|
+
|
|
1106
|
+
💡 Tip: Usually tasks are blocked after starting them.
|
|
1107
|
+
Consider adding notes about what's blocking this.
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
## Error Handling
|
|
1111
|
+
|
|
1112
|
+
### File Read Errors
|
|
1113
|
+
|
|
1114
|
+
```
|
|
1115
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1116
|
+
│ ❌ ERROR │
|
|
1117
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1118
|
+
│ │
|
|
1119
|
+
│ Cannot read PROJECT_PLAN.md │
|
|
1120
|
+
│ │
|
|
1121
|
+
│ Make sure: │
|
|
1122
|
+
│ 1. You're in the correct project directory │
|
|
1123
|
+
│ 2. The file exists (run /planNew if not) │
|
|
1124
|
+
│ 3. You have read permissions │
|
|
1125
|
+
│ │
|
|
1126
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
### File Write Errors
|
|
1130
|
+
|
|
1131
|
+
```
|
|
1132
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1133
|
+
│ ❌ ERROR │
|
|
1134
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1135
|
+
│ │
|
|
1136
|
+
│ Cannot update PROJECT_PLAN.md │
|
|
1137
|
+
│ │
|
|
1138
|
+
│ The file may be: │
|
|
1139
|
+
│ • Open in another program │
|
|
1140
|
+
│ • Read-only │
|
|
1141
|
+
│ • Locked by version control │
|
|
1142
|
+
│ │
|
|
1143
|
+
│ Please check and try again. │
|
|
1144
|
+
│ │
|
|
1145
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
### Malformed Task Format
|
|
1149
|
+
|
|
1150
|
+
```
|
|
1151
|
+
╭──────────────────────────────────────────────────────────────────────────────╮
|
|
1152
|
+
│ ⚠️ WARNING │
|
|
1153
|
+
├──────────────────────────────────────────────────────────────────────────────┤
|
|
1154
|
+
│ │
|
|
1155
|
+
│ Task [task-id] has unexpected format. │
|
|
1156
|
+
│ │
|
|
1157
|
+
│ The update was applied but progress calculations may be inaccurate. │
|
|
1158
|
+
│ Please check the PROJECT_PLAN.md file manually. │
|
|
1159
|
+
│ │
|
|
1160
|
+
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
## Regex Patterns for Parsing
|
|
1164
|
+
|
|
1165
|
+
### Task Header
|
|
1166
|
+
```regex
|
|
1167
|
+
#### (T\d+\.\d+): (.+)
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
### Task Checkbox
|
|
1171
|
+
```regex
|
|
1172
|
+
- \[([ x])\] \*\*Status\*\*: (.+)
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### Dependencies
|
|
1176
|
+
```regex
|
|
1177
|
+
\*\*Dependencies\*\*: (.+)
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
## Examples
|
|
1181
|
+
|
|
1182
|
+
### Example 1: Starting a Task
|
|
1183
|
+
```bash
|
|
1184
|
+
User: /planUpdate T1.1 start
|
|
1185
|
+
|
|
1186
|
+
Output:
|
|
1187
|
+
✅ Task T1.1 updated: TODO → IN_PROGRESS 🔄
|
|
1188
|
+
|
|
1189
|
+
📊 Progress: 0% → 0% (no change)
|
|
1190
|
+
|
|
1191
|
+
You're now working on:
|
|
1192
|
+
T1.1: Project Setup
|
|
1193
|
+
Complexity: Low
|
|
1194
|
+
Estimated: 2 hours
|
|
1195
|
+
|
|
1196
|
+
Good luck! Run /planUpdate T1.1 done when finished.
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
### Example 2: Completing a Task
|
|
1200
|
+
```bash
|
|
1201
|
+
User: /planUpdate T1.1 done
|
|
1202
|
+
|
|
1203
|
+
Output:
|
|
1204
|
+
✅ Task T1.1 completed! 🎉
|
|
1205
|
+
|
|
1206
|
+
📊 Progress: 0% → 7% (+7%)
|
|
1207
|
+
|
|
1208
|
+
Overall Status:
|
|
1209
|
+
🟩⬜⬜⬜⬜⬜⬜⬜⬜⬜ 7%
|
|
1210
|
+
|
|
1211
|
+
Total: 14 tasks
|
|
1212
|
+
✅ Done: 1
|
|
1213
|
+
📋 Remaining: 13
|
|
1214
|
+
|
|
1215
|
+
🔓 Unlocked: T1.2 - Database Configuration
|
|
1216
|
+
|
|
1217
|
+
🎯 Next: /planNext (get recommendation)
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
### Example 3: Blocking a Task
|
|
1221
|
+
```bash
|
|
1222
|
+
User: /planUpdate T2.3 block
|
|
1223
|
+
|
|
1224
|
+
Output:
|
|
1225
|
+
🚫 Task T2.3 marked as blocked
|
|
1226
|
+
|
|
1227
|
+
📊 Progress: 35% (no change)
|
|
1228
|
+
|
|
1229
|
+
Overall Status:
|
|
1230
|
+
Total: 14 tasks
|
|
1231
|
+
✅ Done: 5
|
|
1232
|
+
🚫 Blocked: 1
|
|
1233
|
+
📋 Remaining: 8
|
|
1234
|
+
|
|
1235
|
+
💡 Consider:
|
|
1236
|
+
- Document what's blocking this task
|
|
1237
|
+
- Create a task to resolve the blocker
|
|
1238
|
+
- Update dependencies if needed
|
|
1239
|
+
|
|
1240
|
+
Run /planNext to find alternative tasks to work on.
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
## Important Notes
|
|
1244
|
+
|
|
1245
|
+
1. **Always recalculate progress** after any update
|
|
1246
|
+
2. **Be precise with Edit tool** - match exact strings including whitespace
|
|
1247
|
+
3. **Handle multiple status formats** - tasks may have emojis or not
|
|
1248
|
+
4. **Preserve formatting** - don't accidentally change indentation or structure
|
|
1249
|
+
5. **Atomic updates** - if any edit fails, inform user clearly
|
|
1250
|
+
6. **Phase detection** - T1.X = Phase 1, T2.X = Phase 2, etc.
|
|
1251
|
+
|
|
1252
|
+
## Success Criteria
|
|
1253
|
+
|
|
1254
|
+
A successful update should:
|
|
1255
|
+
- ✅ Change task status correctly
|
|
1256
|
+
- ✅ Update checkbox if completing
|
|
1257
|
+
- ✅ Recalculate all progress metrics
|
|
1258
|
+
- ✅ Update progress bar visual
|
|
1259
|
+
- ✅ Update phase progress
|
|
1260
|
+
- ✅ Update "Current Focus"
|
|
1261
|
+
- ✅ Update "Last Updated" date
|
|
1262
|
+
- ✅ Show clear confirmation to user
|
|
1263
|
+
- ✅ Suggest next action
|
|
1264
|
+
- ✅ **Execute Step 8 (auto-sync check) - ALWAYS!**
|
|
1265
|
+
|
|
1266
|
+
## Cloud Integration (v1.2.0+)
|
|
1267
|
+
|
|
1268
|
+
**IMPORTANT: After completing Step 7, you MUST execute Step 8 to check for auto-sync.**
|
|
1269
|
+
|
|
1270
|
+
When cloud config is available, the /planUpdate command automatically syncs task status to cloud after updating the local file.
|
|
1271
|
+
|
|
1272
|
+
---
|
|
1273
|
+
|
|
1274
|
+
## Sync Mode Decision Flow (v1.3.0+)
|
|
1275
|
+
|
|
1276
|
+
After updating the local PROJECT_PLAN.md, Claude MUST determine which sync mode to use:
|
|
1277
|
+
|
|
1278
|
+
**Pseudo-code:**
|
|
1279
|
+
```javascript
|
|
1280
|
+
function determineSyncMode(config) {
|
|
1281
|
+
const cloudConfig = config.cloud || {}
|
|
1282
|
+
const isAuthenticated = !!cloudConfig.apiToken
|
|
1283
|
+
const projectId = cloudConfig.projectId
|
|
1284
|
+
const storageMode = cloudConfig.storageMode || "local"
|
|
1285
|
+
const autoSync = cloudConfig.autoSync || false
|
|
1286
|
+
|
|
1287
|
+
// Check conditions in order of priority
|
|
1288
|
+
if (!isAuthenticated || !projectId) {
|
|
1289
|
+
return { mode: "skip", reason: "not_authenticated_or_linked" }
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// v1.3.0: Hybrid mode takes precedence
|
|
1293
|
+
if (storageMode === "hybrid") {
|
|
1294
|
+
return { mode: "hybrid", reason: "hybrid_mode_enabled" }
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// v1.3.0: Cloud mode (cloud is source of truth)
|
|
1298
|
+
if (storageMode === "cloud") {
|
|
1299
|
+
return { mode: "cloud", reason: "cloud_mode_enabled" }
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// v1.2.0: Legacy auto-sync (simple push)
|
|
1303
|
+
if (autoSync === true) {
|
|
1304
|
+
return { mode: "auto_sync", reason: "auto_sync_enabled" }
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Default: Local only
|
|
1308
|
+
return { mode: "local", reason: "local_mode" }
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
**Mode Behaviors:**
|
|
1313
|
+
|
|
1314
|
+
| Mode | Behavior | When to Use |
|
|
1315
|
+
|------|----------|-------------|
|
|
1316
|
+
| `local` | No cloud sync | Offline work, no cloud account |
|
|
1317
|
+
| `auto_sync` | Simple push (v1.2.0) | Quick sync without conflict detection |
|
|
1318
|
+
| `cloud` | Pull-then-push, cloud wins | Team projects, cloud is authoritative |
|
|
1319
|
+
| `hybrid` | Pull-merge-push with smart merge | Collaborative work, preserve local changes |
|
|
1320
|
+
|
|
1321
|
+
**Instructions for Claude:**
|
|
1322
|
+
|
|
1323
|
+
1. After Step 7 (local update), call `determineSyncMode(config)`
|
|
1324
|
+
2. Based on result, execute the appropriate sync:
|
|
1325
|
+
- `skip` → No sync, just show confirmation
|
|
1326
|
+
- `local` → No sync, just show confirmation
|
|
1327
|
+
- `auto_sync` → Execute Step 8 (simple PATCH)
|
|
1328
|
+
- `cloud` → Execute Step 8-Cloud (pull first, cloud wins)
|
|
1329
|
+
- `hybrid` → Execute Step 8-Hybrid (pull-merge-push with smart merge)
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
## Hybrid Sync Mode (v1.3.0+)
|
|
1334
|
+
|
|
1335
|
+
When `storageMode: "hybrid"` is configured, the /planUpdate command implements a **pull-before-push** pattern to enable smart merging of concurrent changes.
|
|
1336
|
+
|
|
1337
|
+
### Integration with Smart Merge Skill
|
|
1338
|
+
|
|
1339
|
+
The hybrid sync mode uses the **`skills/smart-merge/SKILL.md`** algorithm for conflict detection and resolution. Key functions used:
|
|
1340
|
+
|
|
1341
|
+
| Function | Purpose | When Called |
|
|
1342
|
+
|----------|---------|-------------|
|
|
1343
|
+
| `smartMerge()` | Core merge algorithm | After pulling cloud state |
|
|
1344
|
+
| `normalizeStatus()` | Normalize status strings | Before comparison |
|
|
1345
|
+
| `buildMergeContext()` | Create merge context | With local and cloud data |
|
|
1346
|
+
| `detectChanges()` | Detect what changed | During context building |
|
|
1347
|
+
|
|
1348
|
+
**Integration Flow:**
|
|
1349
|
+
```
|
|
1350
|
+
/planUpdate T1.1 done
|
|
1351
|
+
│
|
|
1352
|
+
├─→ Update local PROJECT_PLAN.md
|
|
1353
|
+
│
|
|
1354
|
+
├─→ Pull cloud state (GET /projects/:id/tasks/:taskId)
|
|
1355
|
+
│
|
|
1356
|
+
├─→ Call smartMerge() from smart-merge skill
|
|
1357
|
+
│ │
|
|
1358
|
+
│ ├─→ buildMergeContext(local, cloud, lastSyncedAt)
|
|
1359
|
+
│ │
|
|
1360
|
+
│ ├─→ normalizeStatus() for comparison
|
|
1361
|
+
│ │
|
|
1362
|
+
│ └─→ Return: AUTO_MERGE | CONFLICT | NO_CHANGE
|
|
1363
|
+
│
|
|
1364
|
+
├─→ If AUTO_MERGE: Push to cloud
|
|
1365
|
+
│
|
|
1366
|
+
├─→ If CONFLICT: Show conflict UI (T6.4)
|
|
1367
|
+
│
|
|
1368
|
+
└─→ Update lastSyncedAt on success
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
### Storage Mode Check
|
|
1372
|
+
|
|
1373
|
+
Before proceeding with cloud sync, check the storage mode:
|
|
1374
|
+
|
|
1375
|
+
**Pseudo-code:**
|
|
1376
|
+
```javascript
|
|
1377
|
+
const cloudConfig = config.cloud || {}
|
|
1378
|
+
const storageMode = cloudConfig.storageMode || "local" // Default to local-only
|
|
1379
|
+
|
|
1380
|
+
// Storage modes:
|
|
1381
|
+
// - "local" → No auto-sync, just update file
|
|
1382
|
+
// - "cloud" → Cloud is source of truth, always sync
|
|
1383
|
+
// - "hybrid" → Pull-before-push with smart merge (v1.3.0)
|
|
1384
|
+
|
|
1385
|
+
if (storageMode === "hybrid" && isAuthenticated && projectId) {
|
|
1386
|
+
// Use pull-before-push flow (Step 8-Hybrid)
|
|
1387
|
+
await hybridSync(taskId, newStatus, cloudConfig, t)
|
|
1388
|
+
} else if (storageMode === "cloud" && isAuthenticated && projectId) {
|
|
1389
|
+
// Direct push (existing v1.2.0 behavior)
|
|
1390
|
+
await syncTaskToCloud(taskId, newStatus, cloudConfig, t)
|
|
1391
|
+
} else if (autoSync && isAuthenticated && projectId) {
|
|
1392
|
+
// Legacy auto-sync (for backwards compatibility)
|
|
1393
|
+
await syncTaskToCloud(taskId, newStatus, cloudConfig, t)
|
|
1394
|
+
}
|
|
1395
|
+
// else: local mode, no sync
|
|
1396
|
+
```
|
|
1397
|
+
|
|
1398
|
+
---
|
|
1399
|
+
|
|
1400
|
+
### Step 8-Hybrid: Pull-Before-Push Sync (v1.3.0)
|
|
1401
|
+
|
|
1402
|
+
When in hybrid mode, always pull cloud state before pushing local changes to detect and handle concurrent modifications.
|
|
1403
|
+
|
|
1404
|
+
#### Step 8-Hybrid-A: Pull Cloud State
|
|
1405
|
+
|
|
1406
|
+
First, fetch the current cloud state for the specific task.
|
|
1407
|
+
|
|
1408
|
+
**Pseudo-code:**
|
|
1409
|
+
```javascript
|
|
1410
|
+
async function hybridSync(taskId, newLocalStatus, cloudConfig, t) {
|
|
1411
|
+
const projectId = cloudConfig.projectId
|
|
1412
|
+
const apiToken = cloudConfig.apiToken
|
|
1413
|
+
const apiUrl = cloudConfig.apiUrl || "https://api.planflow.tools"
|
|
1414
|
+
const lastSyncedAt = cloudConfig.lastSyncedAt
|
|
1415
|
+
|
|
1416
|
+
// Show syncing indicator
|
|
1417
|
+
console.log("")
|
|
1418
|
+
console.log(t.commands.update.hybridSyncing || "🔄 Syncing with cloud (hybrid mode)...")
|
|
1419
|
+
|
|
1420
|
+
// Step 1: PULL - Get cloud state for this task
|
|
1421
|
+
console.log(t.commands.update.hybridPulling || " ↓ Pulling cloud state...")
|
|
1422
|
+
|
|
1423
|
+
const pullResponse = await fetch(
|
|
1424
|
+
`${apiUrl}/projects/${projectId}/tasks/${taskId}`,
|
|
1425
|
+
{
|
|
1426
|
+
method: "GET",
|
|
1427
|
+
headers: {
|
|
1428
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
1429
|
+
"Accept": "application/json"
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
if (!pullResponse.ok) {
|
|
1435
|
+
if (pullResponse.status === 404) {
|
|
1436
|
+
// Task doesn't exist on cloud yet - safe to push
|
|
1437
|
+
console.log(t.commands.update.hybridTaskNew || " → Task is new, pushing...")
|
|
1438
|
+
return await pushTaskToCloud(taskId, newLocalStatus, cloudConfig, t)
|
|
1439
|
+
}
|
|
1440
|
+
// Other error - fall back to local-only
|
|
1441
|
+
console.log(t.commands.update.hybridPullFailed || " ⚠️ Could not fetch cloud state")
|
|
1442
|
+
console.log(t.commands.update.hybridLocalOnly || " → Changes saved locally only")
|
|
1443
|
+
return
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const cloudTask = pullResponse.data.task
|
|
1447
|
+
const cloudStatus = cloudTask.status
|
|
1448
|
+
const cloudUpdatedAt = cloudTask.updatedAt
|
|
1449
|
+
const cloudUpdatedBy = cloudTask.updatedBy || "cloud"
|
|
1450
|
+
|
|
1451
|
+
// Step 2: COMPARE - Check for conflicts
|
|
1452
|
+
const comparison = compareTaskStates({
|
|
1453
|
+
taskId,
|
|
1454
|
+
localStatus: newLocalStatus,
|
|
1455
|
+
localUpdatedAt: new Date().toISOString(),
|
|
1456
|
+
localUpdatedBy: "local",
|
|
1457
|
+
cloudStatus,
|
|
1458
|
+
cloudUpdatedAt,
|
|
1459
|
+
cloudUpdatedBy,
|
|
1460
|
+
lastSyncedAt
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
// Step 3: Handle based on comparison result
|
|
1464
|
+
if (comparison.result === "NO_CONFLICT") {
|
|
1465
|
+
// Same status or cloud hasn't changed - safe to push
|
|
1466
|
+
console.log(t.commands.update.hybridNoConflict || " ✓ No conflicts detected")
|
|
1467
|
+
return await pushTaskToCloud(taskId, newLocalStatus, cloudConfig, t)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
if (comparison.result === "AUTO_MERGE") {
|
|
1471
|
+
// Cloud changed different field or compatible change
|
|
1472
|
+
console.log(t.commands.update.hybridAutoMerge || " ✓ Auto-merged changes")
|
|
1473
|
+
return await pushTaskToCloud(taskId, newLocalStatus, cloudConfig, t)
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
if (comparison.result === "CONFLICT") {
|
|
1477
|
+
// Real conflict - both changed the same task to different values
|
|
1478
|
+
console.log(t.commands.update.hybridConflict || " ⚠️ Conflict detected!")
|
|
1479
|
+
|
|
1480
|
+
// Store conflict info for resolution (T6.4 will handle UI)
|
|
1481
|
+
return {
|
|
1482
|
+
conflict: true,
|
|
1483
|
+
taskId,
|
|
1484
|
+
local: { status: newLocalStatus, updatedAt: new Date().toISOString() },
|
|
1485
|
+
cloud: { status: cloudStatus, updatedAt: cloudUpdatedAt, updatedBy: cloudUpdatedBy },
|
|
1486
|
+
message: t.commands.update.hybridConflictMessage ||
|
|
1487
|
+
`Task ${taskId} was modified on cloud. Use /pfSyncPush to resolve.`
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
```
|
|
1492
|
+
|
|
1493
|
+
**Bash Implementation for Pull:**
|
|
1494
|
+
|
|
1495
|
+
```bash
|
|
1496
|
+
API_URL="https://api.planflow.tools"
|
|
1497
|
+
TOKEN="$API_TOKEN"
|
|
1498
|
+
PROJECT_ID="$PROJECT_ID"
|
|
1499
|
+
TASK_ID="T1.1"
|
|
1500
|
+
|
|
1501
|
+
# Pull cloud state for specific task
|
|
1502
|
+
echo " ↓ Pulling cloud state..."
|
|
1503
|
+
PULL_RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
1504
|
+
--connect-timeout 5 \
|
|
1505
|
+
--max-time 10 \
|
|
1506
|
+
-X GET \
|
|
1507
|
+
-H "Accept: application/json" \
|
|
1508
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
1509
|
+
"${API_URL}/projects/${PROJECT_ID}/tasks/${TASK_ID}")
|
|
1510
|
+
|
|
1511
|
+
PULL_HTTP_CODE=$(echo "$PULL_RESPONSE" | tail -n1)
|
|
1512
|
+
PULL_BODY=$(echo "$PULL_RESPONSE" | sed '$d')
|
|
1513
|
+
|
|
1514
|
+
if [ "$PULL_HTTP_CODE" -eq 404 ]; then
|
|
1515
|
+
# Task is new on cloud
|
|
1516
|
+
echo " → Task is new, pushing..."
|
|
1517
|
+
# Proceed to push
|
|
1518
|
+
elif [ "$PULL_HTTP_CODE" -ge 200 ] && [ "$PULL_HTTP_CODE" -lt 300 ]; then
|
|
1519
|
+
# Parse cloud status
|
|
1520
|
+
CLOUD_STATUS=$(echo "$PULL_BODY" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
1521
|
+
CLOUD_UPDATED_AT=$(echo "$PULL_BODY" | grep -o '"updatedAt":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
1522
|
+
|
|
1523
|
+
echo " Cloud status: $CLOUD_STATUS (updated: $CLOUD_UPDATED_AT)"
|
|
1524
|
+
# Compare and decide
|
|
1525
|
+
else
|
|
1526
|
+
echo " ⚠️ Could not fetch cloud state (HTTP $PULL_HTTP_CODE)"
|
|
1527
|
+
echo " → Changes saved locally only"
|
|
1528
|
+
exit 0
|
|
1529
|
+
fi
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
---
|
|
1533
|
+
|
|
1534
|
+
#### Step 8-Hybrid-B: Compare Task States
|
|
1535
|
+
|
|
1536
|
+
Compare local and cloud states to determine if there's a conflict.
|
|
1537
|
+
|
|
1538
|
+
**Pseudo-code:**
|
|
1539
|
+
```javascript
|
|
1540
|
+
function compareTaskStates(params) {
|
|
1541
|
+
const {
|
|
1542
|
+
taskId,
|
|
1543
|
+
localStatus,
|
|
1544
|
+
localUpdatedAt,
|
|
1545
|
+
cloudStatus,
|
|
1546
|
+
cloudUpdatedAt,
|
|
1547
|
+
lastSyncedAt
|
|
1548
|
+
} = params
|
|
1549
|
+
|
|
1550
|
+
// Case 1: Same status - no conflict
|
|
1551
|
+
if (localStatus === cloudStatus) {
|
|
1552
|
+
return { result: "NO_CONFLICT", reason: "same_status" }
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Case 2: Cloud hasn't changed since last sync
|
|
1556
|
+
if (lastSyncedAt && new Date(cloudUpdatedAt) <= new Date(lastSyncedAt)) {
|
|
1557
|
+
return { result: "NO_CONFLICT", reason: "cloud_unchanged" }
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Case 3: Cloud changed but to same value we want
|
|
1561
|
+
if (localStatus === cloudStatus) {
|
|
1562
|
+
return { result: "AUTO_MERGE", reason: "convergent_change" }
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Case 4: Real conflict - cloud has different status than what we want
|
|
1566
|
+
// AND cloud was updated after our last sync
|
|
1567
|
+
if (new Date(cloudUpdatedAt) > new Date(lastSyncedAt || 0)) {
|
|
1568
|
+
return {
|
|
1569
|
+
result: "CONFLICT",
|
|
1570
|
+
reason: "concurrent_modification",
|
|
1571
|
+
localStatus,
|
|
1572
|
+
cloudStatus,
|
|
1573
|
+
cloudUpdatedAt
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Default: safe to push
|
|
1578
|
+
return { result: "NO_CONFLICT", reason: "local_newer" }
|
|
1579
|
+
}
|
|
1580
|
+
```
|
|
1581
|
+
|
|
1582
|
+
**Comparison Rules:**
|
|
1583
|
+
|
|
1584
|
+
| Local Status | Cloud Status | Cloud Updated After Sync? | Result |
|
|
1585
|
+
|--------------|--------------|---------------------------|--------|
|
|
1586
|
+
| DONE | DONE | Any | NO_CONFLICT (same) |
|
|
1587
|
+
| DONE | TODO | No | NO_CONFLICT (push) |
|
|
1588
|
+
| DONE | TODO | Yes | CONFLICT |
|
|
1589
|
+
| DONE | IN_PROGRESS | Yes | CONFLICT |
|
|
1590
|
+
| IN_PROGRESS | DONE | Yes | CONFLICT |
|
|
1591
|
+
| IN_PROGRESS | BLOCKED | Yes | CONFLICT |
|
|
1592
|
+
| Any | (404 Not Found) | N/A | NO_CONFLICT (new) |
|
|
1593
|
+
|
|
1594
|
+
---
|
|
1595
|
+
|
|
1596
|
+
#### Step 8-Hybrid-C: Push After Successful Compare
|
|
1597
|
+
|
|
1598
|
+
If no conflict, push the local change to cloud.
|
|
1599
|
+
|
|
1600
|
+
**Pseudo-code:**
|
|
1601
|
+
```javascript
|
|
1602
|
+
async function pushTaskToCloud(taskId, newStatus, cloudConfig, t) {
|
|
1603
|
+
const projectId = cloudConfig.projectId
|
|
1604
|
+
const apiToken = cloudConfig.apiToken
|
|
1605
|
+
const apiUrl = cloudConfig.apiUrl || "https://api.planflow.tools"
|
|
1606
|
+
|
|
1607
|
+
console.log(t.commands.update.hybridPushing || " ↑ Pushing local changes...")
|
|
1608
|
+
|
|
1609
|
+
const pushResponse = await fetch(
|
|
1610
|
+
`${apiUrl}/projects/${projectId}/tasks/${taskId}`,
|
|
1611
|
+
{
|
|
1612
|
+
method: "PATCH",
|
|
1613
|
+
headers: {
|
|
1614
|
+
"Content-Type": "application/json",
|
|
1615
|
+
"Authorization": `Bearer ${apiToken}`
|
|
1616
|
+
},
|
|
1617
|
+
body: JSON.stringify({ status: newStatus })
|
|
1618
|
+
}
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
if (pushResponse.ok) {
|
|
1622
|
+
// Update lastSyncedAt
|
|
1623
|
+
updateLastSyncedAt(new Date().toISOString())
|
|
1624
|
+
console.log(t.commands.update.hybridSyncSuccess || "☁️ ✅ Synced to cloud (hybrid)")
|
|
1625
|
+
return { success: true }
|
|
1626
|
+
} else {
|
|
1627
|
+
console.log(t.commands.update.hybridPushFailed || "☁️ ⚠️ Push failed")
|
|
1628
|
+
return { success: false, error: pushResponse.status }
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
```
|
|
1632
|
+
|
|
1633
|
+
**Bash Implementation for Push:**
|
|
1634
|
+
|
|
1635
|
+
```bash
|
|
1636
|
+
# Push local change to cloud
|
|
1637
|
+
echo " ↑ Pushing local changes..."
|
|
1638
|
+
PUSH_RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
1639
|
+
--connect-timeout 5 \
|
|
1640
|
+
--max-time 10 \
|
|
1641
|
+
-X PATCH \
|
|
1642
|
+
-H "Content-Type: application/json" \
|
|
1643
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
1644
|
+
-d "{\"status\": \"$NEW_STATUS\"}" \
|
|
1645
|
+
"${API_URL}/projects/${PROJECT_ID}/tasks/${TASK_ID}")
|
|
1646
|
+
|
|
1647
|
+
PUSH_HTTP_CODE=$(echo "$PUSH_RESPONSE" | tail -n1)
|
|
1648
|
+
|
|
1649
|
+
if [ "$PUSH_HTTP_CODE" -ge 200 ] && [ "$PUSH_HTTP_CODE" -lt 300 ]; then
|
|
1650
|
+
echo "☁️ ✅ Synced to cloud (hybrid)"
|
|
1651
|
+
else
|
|
1652
|
+
echo "☁️ ⚠️ Push failed (HTTP $PUSH_HTTP_CODE)"
|
|
1653
|
+
fi
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
---
|
|
1657
|
+
|
|
1658
|
+
#### Step 8-Hybrid-D: Handle Conflicts (Basic)
|
|
1659
|
+
|
|
1660
|
+
For v1.3.0, display a basic conflict message. The rich conflict UI (T6.4) will be implemented separately.
|
|
1661
|
+
|
|
1662
|
+
**Pseudo-code:**
|
|
1663
|
+
```javascript
|
|
1664
|
+
function handleConflict(conflict, t) {
|
|
1665
|
+
console.log("")
|
|
1666
|
+
console.log(t.commands.update.hybridConflictDetected || "⚠️ Sync Conflict Detected!")
|
|
1667
|
+
console.log("")
|
|
1668
|
+
console.log(`Task: ${conflict.taskId}`)
|
|
1669
|
+
console.log(` Local: ${conflict.local.status}`)
|
|
1670
|
+
console.log(` Cloud: ${conflict.cloud.status} (by ${conflict.cloud.updatedBy})`)
|
|
1671
|
+
console.log("")
|
|
1672
|
+
console.log(t.commands.update.hybridConflictHint || "💡 To resolve:")
|
|
1673
|
+
console.log(" /pfSyncPushPull --force → Keep cloud version")
|
|
1674
|
+
console.log(" /pfSyncPushPush --force → Keep local version")
|
|
1675
|
+
console.log("")
|
|
1676
|
+
console.log(t.commands.update.hybridLocalSaved || "📝 Local changes saved to PROJECT_PLAN.md")
|
|
1677
|
+
}
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
**Example Conflict Output:**
|
|
1681
|
+
|
|
1682
|
+
```
|
|
1683
|
+
🔄 Syncing with cloud (hybrid mode)...
|
|
1684
|
+
↓ Pulling cloud state...
|
|
1685
|
+
⚠️ Conflict detected!
|
|
1686
|
+
|
|
1687
|
+
⚠️ Sync Conflict Detected!
|
|
1688
|
+
|
|
1689
|
+
Task: T1.2
|
|
1690
|
+
Local: DONE
|
|
1691
|
+
Cloud: BLOCKED (by teammate@example.com)
|
|
1692
|
+
|
|
1693
|
+
💡 To resolve:
|
|
1694
|
+
/pfSyncPushPull --force → Keep cloud version
|
|
1695
|
+
/pfSyncPushPush --force → Keep local version
|
|
1696
|
+
|
|
1697
|
+
📝 Local changes saved to PROJECT_PLAN.md
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
---
|
|
1701
|
+
|
|
1702
|
+
### Complete Hybrid Sync Flow
|
|
1703
|
+
|
|
1704
|
+
**Full Flow Diagram:**
|
|
1705
|
+
|
|
1706
|
+
```
|
|
1707
|
+
/planUpdate T1.1 done
|
|
1708
|
+
│
|
|
1709
|
+
▼
|
|
1710
|
+
┌─────────────────────────────┐
|
|
1711
|
+
│ 1. Update local file │
|
|
1712
|
+
│ PROJECT_PLAN.md │
|
|
1713
|
+
└──────────────┬──────────────┘
|
|
1714
|
+
│
|
|
1715
|
+
▼
|
|
1716
|
+
┌─────────────────────────────┐
|
|
1717
|
+
│ 2. Check storage mode │
|
|
1718
|
+
│ storageMode === "hybrid" │
|
|
1719
|
+
└──────────────┬──────────────┘
|
|
1720
|
+
│ Yes
|
|
1721
|
+
▼
|
|
1722
|
+
┌─────────────────────────────┐
|
|
1723
|
+
│ 3. PULL cloud state │
|
|
1724
|
+
│ GET /tasks/{taskId} │
|
|
1725
|
+
└──────────────┬──────────────┘
|
|
1726
|
+
│
|
|
1727
|
+
▼
|
|
1728
|
+
┌─────────────────────────────┐
|
|
1729
|
+
│ 4. Compare states │
|
|
1730
|
+
│ local vs cloud │
|
|
1731
|
+
└──────────────┬──────────────┘
|
|
1732
|
+
│
|
|
1733
|
+
┌──────┴──────┐
|
|
1734
|
+
│ │
|
|
1735
|
+
▼ ▼
|
|
1736
|
+
NO_CONFLICT CONFLICT
|
|
1737
|
+
│ │
|
|
1738
|
+
▼ ▼
|
|
1739
|
+
┌───────────────┐ ┌───────────────┐
|
|
1740
|
+
│ 5. PUSH │ │ 5. Show │
|
|
1741
|
+
│ changes │ │ conflict │
|
|
1742
|
+
│ │ │ message │
|
|
1743
|
+
└───────┬───────┘ └───────┬───────┘
|
|
1744
|
+
│ │
|
|
1745
|
+
▼ ▼
|
|
1746
|
+
✅ Synced 📝 Local saved
|
|
1747
|
+
⚠️ Needs resolve
|
|
1748
|
+
```
|
|
1749
|
+
|
|
1750
|
+
---
|
|
1751
|
+
|
|
1752
|
+
### Hybrid Sync Translation Keys
|
|
1753
|
+
|
|
1754
|
+
Add these to `locales/en.json` and `locales/ka.json`:
|
|
1755
|
+
|
|
1756
|
+
**English:**
|
|
1757
|
+
```json
|
|
1758
|
+
{
|
|
1759
|
+
"commands": {
|
|
1760
|
+
"update": {
|
|
1761
|
+
"hybridSyncing": "🔄 Syncing with cloud (hybrid mode)...",
|
|
1762
|
+
"hybridPulling": " ↓ Pulling cloud state...",
|
|
1763
|
+
"hybridPushing": " ↑ Pushing local changes...",
|
|
1764
|
+
"hybridNoConflict": " ✓ No conflicts detected",
|
|
1765
|
+
"hybridAutoMerge": " ✓ Auto-merged changes",
|
|
1766
|
+
"hybridConflict": " ⚠️ Conflict detected!",
|
|
1767
|
+
"hybridTaskNew": " → Task is new, pushing...",
|
|
1768
|
+
"hybridPullFailed": " ⚠️ Could not fetch cloud state",
|
|
1769
|
+
"hybridLocalOnly": " → Changes saved locally only",
|
|
1770
|
+
"hybridSyncSuccess": "☁️ ✅ Synced to cloud (hybrid)",
|
|
1771
|
+
"hybridPushFailed": "☁️ ⚠️ Push failed",
|
|
1772
|
+
"hybridConflictDetected": "⚠️ Sync Conflict Detected!",
|
|
1773
|
+
"hybridConflictHint": "💡 To resolve:",
|
|
1774
|
+
"hybridLocalSaved": "📝 Local changes saved to PROJECT_PLAN.md",
|
|
1775
|
+
"hybridConflictMessage": "Task was modified on cloud. Use /pfSyncPush to resolve."
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
**Georgian:**
|
|
1782
|
+
```json
|
|
1783
|
+
{
|
|
1784
|
+
"commands": {
|
|
1785
|
+
"update": {
|
|
1786
|
+
"hybridSyncing": "🔄 სინქრონიზაცია ქლაუდთან (ჰიბრიდული რეჟიმი)...",
|
|
1787
|
+
"hybridPulling": " ↓ ქლაუდის მდგომარეობის მიღება...",
|
|
1788
|
+
"hybridPushing": " ↑ ლოკალური ცვლილებების ატვირთვა...",
|
|
1789
|
+
"hybridNoConflict": " ✓ კონფლიქტი არ აღმოჩნდა",
|
|
1790
|
+
"hybridAutoMerge": " ✓ ცვლილებები ავტომატურად გაერთიანდა",
|
|
1791
|
+
"hybridConflict": " ⚠️ კონფლიქტი აღმოჩნდა!",
|
|
1792
|
+
"hybridTaskNew": " → ამოცანა ახალია, იტვირთება...",
|
|
1793
|
+
"hybridPullFailed": " ⚠️ ქლაუდის მდგომარეობის მიღება ვერ მოხერხდა",
|
|
1794
|
+
"hybridLocalOnly": " → ცვლილებები შენახულია მხოლოდ ლოკალურად",
|
|
1795
|
+
"hybridSyncSuccess": "☁️ ✅ სინქრონიზებულია ქლაუდთან (ჰიბრიდული)",
|
|
1796
|
+
"hybridPushFailed": "☁️ ⚠️ ატვირთვა ვერ მოხერხდა",
|
|
1797
|
+
"hybridConflictDetected": "⚠️ სინქრონიზაციის კონფლიქტი აღმოჩნდა!",
|
|
1798
|
+
"hybridConflictHint": "💡 მოსაგვარებლად:",
|
|
1799
|
+
"hybridLocalSaved": "📝 ლოკალური ცვლილებები შენახულია PROJECT_PLAN.md-ში",
|
|
1800
|
+
"hybridConflictMessage": "ამოცანა შეიცვალა ქლაუდში. გამოიყენეთ /pfSyncPush მოსაგვარებლად."
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
---
|
|
1807
|
+
|
|
1808
|
+
### Testing Hybrid Sync
|
|
1809
|
+
|
|
1810
|
+
```bash
|
|
1811
|
+
# Test 1: Hybrid mode - no conflict (cloud unchanged)
|
|
1812
|
+
# Config: storageMode: "hybrid", authenticated, linked
|
|
1813
|
+
/planUpdate T1.1 done
|
|
1814
|
+
# Expected: Pull → No conflict → Push → Success
|
|
1815
|
+
|
|
1816
|
+
# Test 2: Hybrid mode - new task on cloud
|
|
1817
|
+
# Task exists locally but not on cloud (404)
|
|
1818
|
+
/planUpdate T1.1 done
|
|
1819
|
+
# Expected: Pull (404) → Push as new → Success
|
|
1820
|
+
|
|
1821
|
+
# Test 3: Hybrid mode - conflict
|
|
1822
|
+
# Cloud has T1.1 as BLOCKED, local wants DONE
|
|
1823
|
+
/planUpdate T1.1 done
|
|
1824
|
+
# Expected: Pull → Conflict detected → Show resolution options
|
|
1825
|
+
|
|
1826
|
+
# Test 4: Hybrid mode - same status (no-op)
|
|
1827
|
+
# Both local and cloud have T1.1 as DONE
|
|
1828
|
+
/planUpdate T1.1 done
|
|
1829
|
+
# Expected: Pull → Same status → Skip push → Success
|
|
1830
|
+
|
|
1831
|
+
# Test 5: Hybrid mode - network error on pull
|
|
1832
|
+
/planUpdate T1.1 done
|
|
1833
|
+
# Expected: Pull fails → Save locally → Warn user
|
|
1834
|
+
|
|
1835
|
+
# Test 6: Non-hybrid mode (backwards compatibility)
|
|
1836
|
+
# Config: storageMode: "local" or autoSync: true
|
|
1837
|
+
/planUpdate T1.1 done
|
|
1838
|
+
# Expected: Original v1.2.0 behavior (direct push)
|
|
1839
|
+
```
|
|
1840
|
+
|
|
1841
|
+
---
|
|
1842
|
+
|
|
1843
|
+
## Offline Fallback Handling (v1.3.0)
|
|
1844
|
+
|
|
1845
|
+
When network is unavailable or API calls fail, the /planUpdate command should gracefully degrade to local-only mode while queuing changes for later sync.
|
|
1846
|
+
|
|
1847
|
+
### Offline Detection
|
|
1848
|
+
|
|
1849
|
+
**Pseudo-code:**
|
|
1850
|
+
```javascript
|
|
1851
|
+
async function isOnline(apiUrl) {
|
|
1852
|
+
try {
|
|
1853
|
+
const response = await fetch(`${apiUrl}/health`, {
|
|
1854
|
+
method: "HEAD",
|
|
1855
|
+
timeout: 3000 // 3 second timeout
|
|
1856
|
+
})
|
|
1857
|
+
return response.ok
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
return false
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
**Bash Implementation:**
|
|
1865
|
+
```bash
|
|
1866
|
+
# Quick connectivity check
|
|
1867
|
+
API_URL="https://api.planflow.tools"
|
|
1868
|
+
ONLINE=$(curl -s --connect-timeout 3 --max-time 5 -o /dev/null -w "%{http_code}" "${API_URL}/health" 2>/dev/null)
|
|
1869
|
+
|
|
1870
|
+
if [ "$ONLINE" = "200" ]; then
|
|
1871
|
+
echo "Online"
|
|
1872
|
+
else
|
|
1873
|
+
echo "Offline"
|
|
1874
|
+
fi
|
|
1875
|
+
```
|
|
1876
|
+
|
|
1877
|
+
### Pending Sync Queue
|
|
1878
|
+
|
|
1879
|
+
When offline, store pending changes for later synchronization.
|
|
1880
|
+
|
|
1881
|
+
**Queue File Location:** `./.plan-pending-sync.json`
|
|
1882
|
+
|
|
1883
|
+
**Queue Structure:**
|
|
1884
|
+
```json
|
|
1885
|
+
{
|
|
1886
|
+
"pendingChanges": [
|
|
1887
|
+
{
|
|
1888
|
+
"taskId": "T1.1",
|
|
1889
|
+
"newStatus": "DONE",
|
|
1890
|
+
"localUpdatedAt": "2026-02-01T10:00:00Z",
|
|
1891
|
+
"queuedAt": "2026-02-01T10:00:05Z",
|
|
1892
|
+
"attempts": 0
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
"taskId": "T2.3",
|
|
1896
|
+
"newStatus": "IN_PROGRESS",
|
|
1897
|
+
"localUpdatedAt": "2026-02-01T10:05:00Z",
|
|
1898
|
+
"queuedAt": "2026-02-01T10:05:02Z",
|
|
1899
|
+
"attempts": 0
|
|
1900
|
+
}
|
|
1901
|
+
],
|
|
1902
|
+
"lastAttempt": null
|
|
1903
|
+
}
|
|
1904
|
+
```
|
|
1905
|
+
|
|
1906
|
+
### Queueing Changes
|
|
1907
|
+
|
|
1908
|
+
**Pseudo-code:**
|
|
1909
|
+
```javascript
|
|
1910
|
+
async function queuePendingSync(taskId, newStatus) {
|
|
1911
|
+
const queuePath = "./.plan-pending-sync.json"
|
|
1912
|
+
|
|
1913
|
+
let queue = { pendingChanges: [] }
|
|
1914
|
+
if (fileExists(queuePath)) {
|
|
1915
|
+
try {
|
|
1916
|
+
queue = JSON.parse(readFile(queuePath))
|
|
1917
|
+
} catch (e) {
|
|
1918
|
+
queue = { pendingChanges: [] }
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Check if task already in queue
|
|
1923
|
+
const existingIndex = queue.pendingChanges.findIndex(c => c.taskId === taskId)
|
|
1924
|
+
|
|
1925
|
+
const change = {
|
|
1926
|
+
taskId,
|
|
1927
|
+
newStatus,
|
|
1928
|
+
localUpdatedAt: new Date().toISOString(),
|
|
1929
|
+
queuedAt: new Date().toISOString(),
|
|
1930
|
+
attempts: 0
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
if (existingIndex >= 0) {
|
|
1934
|
+
// Update existing entry (latest status wins)
|
|
1935
|
+
queue.pendingChanges[existingIndex] = change
|
|
1936
|
+
} else {
|
|
1937
|
+
// Add new entry
|
|
1938
|
+
queue.pendingChanges.push(change)
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
writeFile(queuePath, JSON.stringify(queue, null, 2))
|
|
1942
|
+
|
|
1943
|
+
return queue.pendingChanges.length
|
|
1944
|
+
}
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
### Processing Pending Queue
|
|
1948
|
+
|
|
1949
|
+
When back online (e.g., next /update or /pfSyncPush), process pending changes:
|
|
1950
|
+
|
|
1951
|
+
**Pseudo-code:**
|
|
1952
|
+
```javascript
|
|
1953
|
+
async function processPendingQueue(config, t) {
|
|
1954
|
+
const queuePath = "./.plan-pending-sync.json"
|
|
1955
|
+
|
|
1956
|
+
if (!fileExists(queuePath)) {
|
|
1957
|
+
return { processed: 0 }
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
const queue = JSON.parse(readFile(queuePath))
|
|
1961
|
+
|
|
1962
|
+
if (queue.pendingChanges.length === 0) {
|
|
1963
|
+
return { processed: 0 }
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
console.log(t.commands.update.hybridProcessingQueue ||
|
|
1967
|
+
`📤 Processing ${queue.pendingChanges.length} pending changes...`)
|
|
1968
|
+
|
|
1969
|
+
const results = {
|
|
1970
|
+
success: [],
|
|
1971
|
+
failed: [],
|
|
1972
|
+
conflicts: []
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
for (const change of queue.pendingChanges) {
|
|
1976
|
+
try {
|
|
1977
|
+
// Use hybrid sync for each pending change
|
|
1978
|
+
const result = await performHybridSync({
|
|
1979
|
+
taskId: change.taskId,
|
|
1980
|
+
newStatus: change.newStatus
|
|
1981
|
+
}, config, t)
|
|
1982
|
+
|
|
1983
|
+
if (result.success) {
|
|
1984
|
+
results.success.push(change.taskId)
|
|
1985
|
+
} else if (result.conflict) {
|
|
1986
|
+
results.conflicts.push({
|
|
1987
|
+
taskId: change.taskId,
|
|
1988
|
+
conflict: result.conflict
|
|
1989
|
+
})
|
|
1990
|
+
} else {
|
|
1991
|
+
results.failed.push(change.taskId)
|
|
1992
|
+
}
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
results.failed.push(change.taskId)
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// Update queue: remove successful, keep failed for retry
|
|
1999
|
+
queue.pendingChanges = queue.pendingChanges.filter(
|
|
2000
|
+
c => !results.success.includes(c.taskId)
|
|
2001
|
+
)
|
|
2002
|
+
queue.lastAttempt = new Date().toISOString()
|
|
2003
|
+
|
|
2004
|
+
if (queue.pendingChanges.length === 0) {
|
|
2005
|
+
// Delete queue file if empty
|
|
2006
|
+
deleteFile(queuePath)
|
|
2007
|
+
} else {
|
|
2008
|
+
writeFile(queuePath, JSON.stringify(queue, null, 2))
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
return results
|
|
2012
|
+
}
|
|
2013
|
+
```
|
|
2014
|
+
|
|
2015
|
+
### Offline Mode Output
|
|
2016
|
+
|
|
2017
|
+
When operating in offline mode:
|
|
2018
|
+
|
|
2019
|
+
```
|
|
2020
|
+
✅ Task T1.2 completed! 🎉
|
|
2021
|
+
|
|
2022
|
+
📊 Progress: 25% → 31% (+6%)
|
|
2023
|
+
|
|
2024
|
+
[... normal output ...]
|
|
2025
|
+
|
|
2026
|
+
🔄 Syncing with cloud (hybrid mode)...
|
|
2027
|
+
⚠️ Network unavailable
|
|
2028
|
+
📝 Changes saved locally
|
|
2029
|
+
📤 Queued for sync when online (1 pending)
|
|
2030
|
+
|
|
2031
|
+
💡 Run /pfSyncPush when back online to push changes
|
|
2032
|
+
|
|
2033
|
+
🎯 Next: /planNext (get recommendation)
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
### Translation Keys for Offline Mode
|
|
2037
|
+
|
|
2038
|
+
Add to `locales/en.json`:
|
|
2039
|
+
```json
|
|
2040
|
+
{
|
|
2041
|
+
"commands": {
|
|
2042
|
+
"update": {
|
|
2043
|
+
"hybridOffline": " ⚠️ Network unavailable",
|
|
2044
|
+
"hybridQueued": " 📤 Queued for sync when online ({count} pending)",
|
|
2045
|
+
"hybridProcessingQueue": "📤 Processing {count} pending changes...",
|
|
2046
|
+
"hybridQueueSuccess": " ✓ {count} pending changes synced",
|
|
2047
|
+
"hybridQueueFailed": " ⚠️ {count} changes failed to sync",
|
|
2048
|
+
"hybridQueueConflicts": " ⚠️ {count} conflicts need resolution",
|
|
2049
|
+
"hybridSyncWhenOnline": "💡 Run /pfSyncPush when back online to push changes"
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
Add to `locales/ka.json`:
|
|
2056
|
+
```json
|
|
2057
|
+
{
|
|
2058
|
+
"commands": {
|
|
2059
|
+
"update": {
|
|
2060
|
+
"hybridOffline": " ⚠️ ქსელი მიუწვდომელია",
|
|
2061
|
+
"hybridQueued": " 📤 რიგში დგას სინქრონიზაციისთვის ({count} მოლოდინში)",
|
|
2062
|
+
"hybridProcessingQueue": "📤 მუშავდება {count} მოლოდინში მყოფი ცვლილება...",
|
|
2063
|
+
"hybridQueueSuccess": " ✓ {count} მოლოდინში მყოფი ცვლილება სინქრონიზდა",
|
|
2064
|
+
"hybridQueueFailed": " ⚠️ {count} ცვლილების სინქრონიზაცია ვერ მოხერხდა",
|
|
2065
|
+
"hybridQueueConflicts": " ⚠️ {count} კონფლიქტი საჭიროებს მოგვარებას",
|
|
2066
|
+
"hybridSyncWhenOnline": "💡 გაუშვით /pfSyncPush როცა ონლაინ იქნებით ცვლილებების ასატვირთად"
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
```
|
|
2071
|
+
|
|
2072
|
+
### Complete Offline Flow
|
|
2073
|
+
|
|
2074
|
+
```
|
|
2075
|
+
┌────────────────────────────────────────┐
|
|
2076
|
+
│ /planUpdate T1.1 done │
|
|
2077
|
+
└─────────────────┬──────────────────────┘
|
|
2078
|
+
│
|
|
2079
|
+
▼
|
|
2080
|
+
┌────────────────────────────────────────┐
|
|
2081
|
+
│ 1. Update local PROJECT_PLAN.md │
|
|
2082
|
+
│ (Always succeeds) │
|
|
2083
|
+
└─────────────────┬──────────────────────┘
|
|
2084
|
+
│
|
|
2085
|
+
▼
|
|
2086
|
+
┌────────────────────────────────────────┐
|
|
2087
|
+
│ 2. Check network connectivity │
|
|
2088
|
+
│ curl --connect-timeout 3 /health │
|
|
2089
|
+
└─────────────────┬──────────────────────┘
|
|
2090
|
+
│
|
|
2091
|
+
┌───────┴───────┐
|
|
2092
|
+
│ │
|
|
2093
|
+
ONLINE OFFLINE
|
|
2094
|
+
│ │
|
|
2095
|
+
▼ ▼
|
|
2096
|
+
┌─────────────┐ ┌─────────────────────┐
|
|
2097
|
+
│ 3a. Process │ │ 3b. Queue change │
|
|
2098
|
+
│ pending │ │ for later sync │
|
|
2099
|
+
│ queue first │ │ │
|
|
2100
|
+
└──────┬──────┘ └──────────┬──────────┘
|
|
2101
|
+
│ │
|
|
2102
|
+
▼ ▼
|
|
2103
|
+
┌─────────────┐ ┌─────────────────────┐
|
|
2104
|
+
│ 4a. Hybrid │ │ 4b. Show "queued" │
|
|
2105
|
+
│ sync new │ │ message │
|
|
2106
|
+
│ change │ │ │
|
|
2107
|
+
└──────┬──────┘ └──────────┬──────────┘
|
|
2108
|
+
│ │
|
|
2109
|
+
└──────────┬──────────┘
|
|
2110
|
+
│
|
|
2111
|
+
▼
|
|
2112
|
+
┌────────────────────────────────────────┐
|
|
2113
|
+
│ 5. Show confirmation │
|
|
2114
|
+
└────────────────────────────────────────┘
|
|
2115
|
+
```
|
|
2116
|
+
|
|
2117
|
+
---
|
|
2118
|
+
|
|
2119
|
+
### Step 8: Auto-Sync to Cloud (REQUIRED CHECK)
|
|
2120
|
+
|
|
2121
|
+
**CRITICAL: Always execute this step after Step 7, even if you think auto-sync might be disabled.**
|
|
2122
|
+
|
|
2123
|
+
After successfully updating the local PROJECT_PLAN.md file, check if auto-sync should be triggered.
|
|
2124
|
+
|
|
2125
|
+
**Pseudo-code:**
|
|
2126
|
+
```javascript
|
|
2127
|
+
// Check if auto-sync conditions are met
|
|
2128
|
+
const cloudConfig = config.cloud || {}
|
|
2129
|
+
const isAuthenticated = !!cloudConfig.apiToken
|
|
2130
|
+
const projectId = cloudConfig.projectId
|
|
2131
|
+
const autoSync = cloudConfig.autoSync || false
|
|
2132
|
+
|
|
2133
|
+
if (isAuthenticated && projectId && autoSync) {
|
|
2134
|
+
// Trigger auto-sync
|
|
2135
|
+
syncTaskToCloud(taskId, newStatus, cloudConfig, t)
|
|
2136
|
+
}
|
|
2137
|
+
```
|
|
2138
|
+
|
|
2139
|
+
**Instructions for Claude:**
|
|
2140
|
+
|
|
2141
|
+
After Step 7 (showing confirmation), check if auto-sync should be triggered:
|
|
2142
|
+
|
|
2143
|
+
1. Read cloud config from loaded config:
|
|
2144
|
+
- `apiToken` - authentication token
|
|
2145
|
+
- `projectId` - linked cloud project ID
|
|
2146
|
+
- `autoSync` - boolean flag to enable auto-sync
|
|
2147
|
+
|
|
2148
|
+
2. If ALL three conditions are met:
|
|
2149
|
+
- User is authenticated (`apiToken` exists)
|
|
2150
|
+
- Project is linked (`projectId` exists)
|
|
2151
|
+
- Auto-sync is enabled (`autoSync === true`)
|
|
2152
|
+
|
|
2153
|
+
3. If conditions met, proceed to auto-sync the task update
|
|
2154
|
+
|
|
2155
|
+
---
|
|
2156
|
+
|
|
2157
|
+
### Step 8a: Sync Task Status to Cloud
|
|
2158
|
+
|
|
2159
|
+
Sync the specific task update to cloud using the PATCH /projects/:id/tasks/:taskId API.
|
|
2160
|
+
|
|
2161
|
+
**Pseudo-code:**
|
|
2162
|
+
```javascript
|
|
2163
|
+
async function syncTaskToCloud(taskId, newStatus, cloudConfig, t) {
|
|
2164
|
+
// Show syncing indicator
|
|
2165
|
+
console.log("")
|
|
2166
|
+
console.log("☁️ Auto-syncing to cloud...")
|
|
2167
|
+
|
|
2168
|
+
// Make API request to update single task
|
|
2169
|
+
const response = makeRequest(
|
|
2170
|
+
"PATCH",
|
|
2171
|
+
`/projects/${cloudConfig.projectId}/tasks/${taskId}`,
|
|
2172
|
+
{ status: newStatus },
|
|
2173
|
+
cloudConfig.apiToken
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
if (response.ok) {
|
|
2177
|
+
// Update lastSyncedAt in config
|
|
2178
|
+
updateLastSyncedAt(new Date().toISOString())
|
|
2179
|
+
|
|
2180
|
+
// Show success (brief)
|
|
2181
|
+
console.log("☁️ ✅ Synced to cloud")
|
|
2182
|
+
} else {
|
|
2183
|
+
// Show error but don't fail the update
|
|
2184
|
+
console.log("☁️ ⚠️ Cloud sync failed (local update succeeded)")
|
|
2185
|
+
|
|
2186
|
+
if (response.status === 401) {
|
|
2187
|
+
console.log(" Token may be expired. Run /pfLogin to re-authenticate.")
|
|
2188
|
+
} else if (response.status === 404) {
|
|
2189
|
+
console.log(" Task not found on cloud. Run /pfSyncPushPush to sync full plan.")
|
|
2190
|
+
} else {
|
|
2191
|
+
console.log(" Try /pfSyncPushPush later to manually sync.")
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
```
|
|
2196
|
+
|
|
2197
|
+
**Bash Implementation:**
|
|
2198
|
+
|
|
2199
|
+
```bash
|
|
2200
|
+
API_URL="https://api.planflow.tools"
|
|
2201
|
+
TOKEN="$API_TOKEN"
|
|
2202
|
+
PROJECT_ID="$PROJECT_ID"
|
|
2203
|
+
TASK_ID="T1.1"
|
|
2204
|
+
NEW_STATUS="DONE"
|
|
2205
|
+
|
|
2206
|
+
# Make API request to update single task by taskId
|
|
2207
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
2208
|
+
--connect-timeout 5 \
|
|
2209
|
+
--max-time 10 \
|
|
2210
|
+
-X PATCH \
|
|
2211
|
+
-H "Content-Type: application/json" \
|
|
2212
|
+
-H "Accept: application/json" \
|
|
2213
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
2214
|
+
-d "{\"status\": \"$NEW_STATUS\"}" \
|
|
2215
|
+
"${API_URL}/projects/${PROJECT_ID}/tasks/${TASK_ID}")
|
|
2216
|
+
|
|
2217
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
|
|
2218
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
2219
|
+
|
|
2220
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
2221
|
+
echo "☁️ ✅ Synced to cloud"
|
|
2222
|
+
else
|
|
2223
|
+
echo "☁️ ⚠️ Cloud sync failed (local update succeeded)"
|
|
2224
|
+
fi
|
|
2225
|
+
```
|
|
2226
|
+
|
|
2227
|
+
**Instructions for Claude:**
|
|
2228
|
+
|
|
2229
|
+
1. Show syncing indicator:
|
|
2230
|
+
```
|
|
2231
|
+
☁️ Auto-syncing to cloud...
|
|
2232
|
+
```
|
|
2233
|
+
|
|
2234
|
+
2. Make API PATCH request to `/projects/{projectId}/tasks/{taskId}`:
|
|
2235
|
+
```bash
|
|
2236
|
+
curl -s -w "\n%{http_code}" \
|
|
2237
|
+
--connect-timeout 5 \
|
|
2238
|
+
--max-time 10 \
|
|
2239
|
+
-X PATCH \
|
|
2240
|
+
-H "Content-Type: application/json" \
|
|
2241
|
+
-H "Authorization: Bearer {TOKEN}" \
|
|
2242
|
+
-d '{"status": "{STATUS}"}' \
|
|
2243
|
+
"https://api.planflow.tools/projects/{PROJECT_ID}/tasks/{TASK_ID}"
|
|
2244
|
+
```
|
|
2245
|
+
|
|
2246
|
+
Map task status to API format:
|
|
2247
|
+
- `start` action → `"IN_PROGRESS"`
|
|
2248
|
+
- `done` action → `"DONE"`
|
|
2249
|
+
- `block` action → `"BLOCKED"`
|
|
2250
|
+
|
|
2251
|
+
3. Handle response:
|
|
2252
|
+
- **Success (200)**: Show "☁️ ✅ Synced to cloud"
|
|
2253
|
+
- **Error**: Show warning but don't fail (local update already succeeded)
|
|
2254
|
+
|
|
2255
|
+
4. Update `lastSyncedAt` in local config on success
|
|
2256
|
+
|
|
2257
|
+
---
|
|
2258
|
+
|
|
2259
|
+
### Step 8b: Update Config After Sync
|
|
2260
|
+
|
|
2261
|
+
Save sync timestamp to config after successful cloud sync.
|
|
2262
|
+
|
|
2263
|
+
**Pseudo-code:**
|
|
2264
|
+
```javascript
|
|
2265
|
+
function updateLastSyncedAt(timestamp) {
|
|
2266
|
+
const localPath = "./.plan-config.json"
|
|
2267
|
+
|
|
2268
|
+
let config = {}
|
|
2269
|
+
if (fileExists(localPath)) {
|
|
2270
|
+
config = JSON.parse(readFile(localPath))
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
if (!config.cloud) {
|
|
2274
|
+
config.cloud = {}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
config.cloud.lastSyncedAt = timestamp
|
|
2278
|
+
|
|
2279
|
+
writeFile(localPath, JSON.stringify(config, null, 2))
|
|
2280
|
+
}
|
|
2281
|
+
```
|
|
2282
|
+
|
|
2283
|
+
**Instructions for Claude:**
|
|
2284
|
+
|
|
2285
|
+
1. Read current `./.plan-config.json`
|
|
2286
|
+
2. Update `cloud.lastSyncedAt` with current timestamp
|
|
2287
|
+
3. Write back config file using Edit or Write tool
|
|
2288
|
+
|
|
2289
|
+
---
|
|
2290
|
+
|
|
2291
|
+
### Auto-Sync Output Examples
|
|
2292
|
+
|
|
2293
|
+
#### Example 1: Successful Auto-Sync
|
|
2294
|
+
|
|
2295
|
+
```
|
|
2296
|
+
✅ Task T1.2 completed! 🎉
|
|
2297
|
+
|
|
2298
|
+
📊 Progress: 25% → 31% (+6%)
|
|
2299
|
+
|
|
2300
|
+
Overall Status:
|
|
2301
|
+
Total: 18
|
|
2302
|
+
✅ Done: 6
|
|
2303
|
+
🔄 In Progress: 1
|
|
2304
|
+
🚫 Blocked: 0
|
|
2305
|
+
📋 Remaining: 11
|
|
2306
|
+
|
|
2307
|
+
🟩🟩🟩⬜⬜⬜⬜⬜⬜⬜ 31%
|
|
2308
|
+
|
|
2309
|
+
☁️ Auto-syncing to cloud...
|
|
2310
|
+
☁️ ✅ Synced to cloud
|
|
2311
|
+
|
|
2312
|
+
🎯 Next: /planNext (get recommendation)
|
|
2313
|
+
```
|
|
2314
|
+
|
|
2315
|
+
#### Example 2: Auto-Sync Disabled (No Output)
|
|
2316
|
+
|
|
2317
|
+
When `autoSync: false` or not set, no cloud sync message appears:
|
|
2318
|
+
|
|
2319
|
+
```
|
|
2320
|
+
✅ Task T1.2 completed! 🎉
|
|
2321
|
+
|
|
2322
|
+
📊 Progress: 25% → 31% (+6%)
|
|
2323
|
+
|
|
2324
|
+
[... normal output ...]
|
|
2325
|
+
|
|
2326
|
+
🎯 Next: /planNext (get recommendation)
|
|
2327
|
+
```
|
|
2328
|
+
|
|
2329
|
+
#### Example 3: Auto-Sync Failed (Graceful Degradation)
|
|
2330
|
+
|
|
2331
|
+
```
|
|
2332
|
+
✅ Task T1.2 completed! 🎉
|
|
2333
|
+
|
|
2334
|
+
📊 Progress: 25% → 31% (+6%)
|
|
2335
|
+
|
|
2336
|
+
[... normal output ...]
|
|
2337
|
+
|
|
2338
|
+
☁️ Auto-syncing to cloud...
|
|
2339
|
+
☁️ ⚠️ Cloud sync failed (local update succeeded)
|
|
2340
|
+
Token may be expired. Run /pfLogin to re-authenticate.
|
|
2341
|
+
|
|
2342
|
+
🎯 Next: /planNext (get recommendation)
|
|
2343
|
+
```
|
|
2344
|
+
|
|
2345
|
+
#### Example 4: Not Authenticated (Silent Skip)
|
|
2346
|
+
|
|
2347
|
+
When user is not authenticated, auto-sync is silently skipped:
|
|
2348
|
+
|
|
2349
|
+
```
|
|
2350
|
+
✅ Task T1.2 completed! 🎉
|
|
2351
|
+
|
|
2352
|
+
📊 Progress: 25% → 31% (+6%)
|
|
2353
|
+
|
|
2354
|
+
[... normal output ...]
|
|
2355
|
+
|
|
2356
|
+
🎯 Next: /planNext (get recommendation)
|
|
2357
|
+
```
|
|
2358
|
+
|
|
2359
|
+
#### Example 5: Georgian Language with Auto-Sync
|
|
2360
|
+
|
|
2361
|
+
```
|
|
2362
|
+
✅ ამოცანა T1.2 დასრულდა! 🎉
|
|
2363
|
+
|
|
2364
|
+
📊 პროგრესი: 25% → 31% (+6%)
|
|
2365
|
+
|
|
2366
|
+
[... Georgian output ...]
|
|
2367
|
+
|
|
2368
|
+
☁️ ავტო-სინქრონიზაცია ქლაუდთან...
|
|
2369
|
+
☁️ ✅ სინქრონიზებულია ქლაუდთან
|
|
2370
|
+
|
|
2371
|
+
🎯 შემდეგი: /planNext (რეკომენდაციის მისაღებად)
|
|
2372
|
+
```
|
|
2373
|
+
|
|
2374
|
+
---
|
|
2375
|
+
|
|
2376
|
+
### Auto-Sync Configuration
|
|
2377
|
+
|
|
2378
|
+
Users enable auto-sync via `/settings` or by editing config directly:
|
|
2379
|
+
|
|
2380
|
+
**Local config (`./.plan-config.json`):**
|
|
2381
|
+
```json
|
|
2382
|
+
{
|
|
2383
|
+
"language": "en",
|
|
2384
|
+
"cloud": {
|
|
2385
|
+
"projectId": "abc123",
|
|
2386
|
+
"autoSync": true,
|
|
2387
|
+
"lastSyncedAt": "2026-01-31T15:30:00Z"
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
```
|
|
2391
|
+
|
|
2392
|
+
**Global config (`~/.config/claude/plan-plugin-config.json`):**
|
|
2393
|
+
```json
|
|
2394
|
+
{
|
|
2395
|
+
"language": "en",
|
|
2396
|
+
"cloud": {
|
|
2397
|
+
"apiToken": "pf_xxx...",
|
|
2398
|
+
"apiUrl": "https://api.planflow.tools",
|
|
2399
|
+
"autoSync": true
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
```
|
|
2403
|
+
|
|
2404
|
+
**Notes:**
|
|
2405
|
+
- `autoSync` defaults to `false` if not set
|
|
2406
|
+
- Local config `projectId` takes precedence (project-specific)
|
|
2407
|
+
- Global config typically stores `apiToken` (shared across projects)
|
|
2408
|
+
- Local config stores `projectId` and `lastSyncedAt` (project-specific)
|
|
2409
|
+
- Configs are MERGED: global provides base, local overrides/extends
|
|
2410
|
+
|
|
2411
|
+
---
|
|
2412
|
+
|
|
2413
|
+
### Error Handling for Auto-Sync
|
|
2414
|
+
|
|
2415
|
+
Auto-sync should NEVER fail the local update. It's a background enhancement.
|
|
2416
|
+
|
|
2417
|
+
**Principles:**
|
|
2418
|
+
1. Local update always completes first
|
|
2419
|
+
2. Cloud sync errors are warnings, not failures
|
|
2420
|
+
3. Network timeouts are short (5s connect, 10s total)
|
|
2421
|
+
4. Errors provide actionable hints
|
|
2422
|
+
5. Uses PATCH endpoint for single task updates
|
|
2423
|
+
|
|
2424
|
+
**Error Scenarios:**
|
|
2425
|
+
|
|
2426
|
+
| Scenario | Behavior |
|
|
2427
|
+
|----------|----------|
|
|
2428
|
+
| Network timeout | Show warning, suggest `/pfSyncPushPush` later |
|
|
2429
|
+
| 401 Unauthorized | Show warning, suggest `/pfLogin` |
|
|
2430
|
+
| 404 Not Found | Show warning, suggest `/pfSyncPushPush` to sync full plan |
|
|
2431
|
+
| 500 Server Error | Show warning, suggest retry later |
|
|
2432
|
+
| Config missing | Silently skip (not authenticated/linked) |
|
|
2433
|
+
|
|
2434
|
+
---
|
|
2435
|
+
|
|
2436
|
+
### Translation Keys for Auto-Sync
|
|
2437
|
+
|
|
2438
|
+
Add these keys to `locales/en.json` and `locales/ka.json`:
|
|
2439
|
+
|
|
2440
|
+
```json
|
|
2441
|
+
{
|
|
2442
|
+
"commands": {
|
|
2443
|
+
"update": {
|
|
2444
|
+
"autoSyncing": "☁️ Auto-syncing to cloud...",
|
|
2445
|
+
"autoSyncSuccess": "☁️ ✅ Synced to cloud",
|
|
2446
|
+
"autoSyncFailed": "☁️ ⚠️ Cloud sync failed (local update succeeded)",
|
|
2447
|
+
"autoSyncTokenExpired": " Token may be expired. Run /pfLogin to re-authenticate.",
|
|
2448
|
+
"autoSyncTaskNotFound": " Task not found on cloud. Run /pfSyncPushPush to sync full plan.",
|
|
2449
|
+
"autoSyncTryLater": " Try /pfSyncPushPush later to manually sync."
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
```
|
|
2454
|
+
|
|
2455
|
+
**Georgian translations:**
|
|
2456
|
+
```json
|
|
2457
|
+
{
|
|
2458
|
+
"commands": {
|
|
2459
|
+
"update": {
|
|
2460
|
+
"autoSyncing": "☁️ ავტო-სინქრონიზაცია ქლაუდთან...",
|
|
2461
|
+
"autoSyncSuccess": "☁️ ✅ სინქრონიზებულია ქლაუდთან",
|
|
2462
|
+
"autoSyncFailed": "☁️ ⚠️ ქლაუდ სინქრონიზაცია ვერ მოხერხდა (ლოკალური განახლება წარმატებულია)",
|
|
2463
|
+
"autoSyncTokenExpired": " ტოკენი შესაძლოა ვადაგასულია. გაუშვით /pfLogin ხელახლა ავთენტიფიკაციისთვის.",
|
|
2464
|
+
"autoSyncTaskNotFound": " ამოცანა ვერ მოიძებნა ქლაუდში. გაუშვით /pfSyncPushPush სრული გეგმის სინქრონიზაციისთვის.",
|
|
2465
|
+
"autoSyncTryLater": " სცადეთ /pfSyncPushPush მოგვიანებით ხელით სინქრონიზაციისთვის."
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
```
|
|
2470
|
+
|
|
2471
|
+
**Instructions for Claude:**
|
|
2472
|
+
|
|
2473
|
+
Use the appropriate translation key when displaying auto-sync messages:
|
|
2474
|
+
- `t.commands.update.autoSyncing` - Starting sync message
|
|
2475
|
+
- `t.commands.update.autoSyncSuccess` - Success message
|
|
2476
|
+
- `t.commands.update.autoSyncFailed` - Failure warning
|
|
2477
|
+
- `t.commands.update.autoSyncTokenExpired` - Token hint
|
|
2478
|
+
- `t.commands.update.autoSyncTaskNotFound` - Task not found hint
|
|
2479
|
+
- `t.commands.update.autoSyncTryLater` - Manual sync hint
|
|
2480
|
+
|
|
2481
|
+
---
|
|
2482
|
+
|
|
2483
|
+
### Testing Auto-Sync
|
|
2484
|
+
|
|
2485
|
+
```bash
|
|
2486
|
+
# Test 1: Auto-sync disabled (default)
|
|
2487
|
+
# Config has autoSync: false or missing
|
|
2488
|
+
/planUpdate T1.1 done
|
|
2489
|
+
# Should NOT show any cloud sync messages
|
|
2490
|
+
|
|
2491
|
+
# Test 2: Auto-sync enabled - success
|
|
2492
|
+
# Config has: autoSync: true, apiToken, projectId
|
|
2493
|
+
/planUpdate T1.1 done
|
|
2494
|
+
# Should show "☁️ Auto-syncing..." then "☁️ ✅ Synced"
|
|
2495
|
+
|
|
2496
|
+
# Test 3: Auto-sync enabled - not authenticated
|
|
2497
|
+
# Config has: autoSync: true, NO apiToken
|
|
2498
|
+
/planUpdate T1.1 done
|
|
2499
|
+
# Should silently skip auto-sync (no messages)
|
|
2500
|
+
|
|
2501
|
+
# Test 4: Auto-sync enabled - not linked
|
|
2502
|
+
# Config has: autoSync: true, apiToken, NO projectId
|
|
2503
|
+
/planUpdate T1.1 done
|
|
2504
|
+
# Should silently skip auto-sync (no messages)
|
|
2505
|
+
|
|
2506
|
+
# Test 5: Auto-sync enabled - network error
|
|
2507
|
+
# Config has: autoSync: true, apiToken, projectId
|
|
2508
|
+
# But API is unreachable
|
|
2509
|
+
/planUpdate T1.1 done
|
|
2510
|
+
# Should show "☁️ ⚠️ Cloud sync failed..."
|
|
2511
|
+
# Local update should still succeed
|
|
2512
|
+
|
|
2513
|
+
# Test 6: Auto-sync enabled - token expired
|
|
2514
|
+
# Config has: autoSync: true, INVALID apiToken, projectId
|
|
2515
|
+
/planUpdate T1.1 done
|
|
2516
|
+
# Should show "☁️ ⚠️ Cloud sync failed..."
|
|
2517
|
+
# With hint about /pfLogin
|
|
2518
|
+
```
|