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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/bin/cli.js +169 -0
  4. package/bin/postinstall.js +87 -0
  5. package/commands/pfActivity/SKILL.md +725 -0
  6. package/commands/pfAssign/SKILL.md +623 -0
  7. package/commands/pfCloudLink/SKILL.md +192 -0
  8. package/commands/pfCloudList/SKILL.md +222 -0
  9. package/commands/pfCloudNew/SKILL.md +187 -0
  10. package/commands/pfCloudUnlink/SKILL.md +152 -0
  11. package/commands/pfComment/SKILL.md +227 -0
  12. package/commands/pfComments/SKILL.md +159 -0
  13. package/commands/pfConnectionStatus/SKILL.md +433 -0
  14. package/commands/pfDiscord/SKILL.md +740 -0
  15. package/commands/pfGithubBranch/SKILL.md +672 -0
  16. package/commands/pfGithubIssue/SKILL.md +963 -0
  17. package/commands/pfGithubLink/SKILL.md +859 -0
  18. package/commands/pfGithubPr/SKILL.md +1335 -0
  19. package/commands/pfGithubUnlink/SKILL.md +401 -0
  20. package/commands/pfLive/SKILL.md +185 -0
  21. package/commands/pfLogin/SKILL.md +249 -0
  22. package/commands/pfLogout/SKILL.md +155 -0
  23. package/commands/pfMyTasks/SKILL.md +198 -0
  24. package/commands/pfNotificationSettings/SKILL.md +619 -0
  25. package/commands/pfNotifications/SKILL.md +420 -0
  26. package/commands/pfNotificationsClear/SKILL.md +421 -0
  27. package/commands/pfReact/SKILL.md +232 -0
  28. package/commands/pfSlack/SKILL.md +659 -0
  29. package/commands/pfSyncPull/SKILL.md +210 -0
  30. package/commands/pfSyncPush/SKILL.md +299 -0
  31. package/commands/pfSyncStatus/SKILL.md +212 -0
  32. package/commands/pfTeamInvite/SKILL.md +161 -0
  33. package/commands/pfTeamList/SKILL.md +253 -0
  34. package/commands/pfTeamRemove/SKILL.md +115 -0
  35. package/commands/pfTeamRole/SKILL.md +160 -0
  36. package/commands/pfTestWebhooks/SKILL.md +722 -0
  37. package/commands/pfUnassign/SKILL.md +134 -0
  38. package/commands/pfWhoami/SKILL.md +258 -0
  39. package/commands/pfWorkload/SKILL.md +219 -0
  40. package/commands/planExportCsv/SKILL.md +106 -0
  41. package/commands/planExportGithub/SKILL.md +222 -0
  42. package/commands/planExportJson/SKILL.md +159 -0
  43. package/commands/planExportSummary/SKILL.md +158 -0
  44. package/commands/planNew/SKILL.md +641 -0
  45. package/commands/planNext/SKILL.md +1200 -0
  46. package/commands/planSettingsAutoSync/SKILL.md +199 -0
  47. package/commands/planSettingsLanguage/SKILL.md +201 -0
  48. package/commands/planSettingsReset/SKILL.md +237 -0
  49. package/commands/planSettingsShow/SKILL.md +482 -0
  50. package/commands/planSpec/SKILL.md +929 -0
  51. package/commands/planUpdate/SKILL.md +2518 -0
  52. package/commands/team/SKILL.md +740 -0
  53. package/locales/en.json +1499 -0
  54. package/locales/ka.json +1499 -0
  55. package/package.json +48 -0
  56. package/templates/PROJECT_PLAN.template.md +157 -0
  57. package/templates/backend-api.template.md +562 -0
  58. package/templates/frontend-spa.template.md +610 -0
  59. package/templates/fullstack.template.md +397 -0
  60. package/templates/ka/backend-api.template.md +562 -0
  61. package/templates/ka/frontend-spa.template.md +610 -0
  62. package/templates/ka/fullstack.template.md +397 -0
  63. package/templates/sections/architecture.md +21 -0
  64. package/templates/sections/overview.md +15 -0
  65. package/templates/sections/tasks.md +22 -0
  66. 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
+ ```