create-arete-workspace 0.2.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 (180) hide show
  1. package/README.md +77 -0
  2. package/bin/arete.js +156 -0
  3. package/bin/create.js +111 -0
  4. package/lib/install-openclaw.js +50 -0
  5. package/lib/scaffold.js +213 -0
  6. package/lib/setup-wizard.js +88 -0
  7. package/lib/updater.js +130 -0
  8. package/package.json +34 -0
  9. package/packages/gatsaeng-os/README.md +36 -0
  10. package/packages/gatsaeng-os/components.json +23 -0
  11. package/packages/gatsaeng-os/eslint.config.mjs +18 -0
  12. package/packages/gatsaeng-os/next.config.ts +7 -0
  13. package/packages/gatsaeng-os/package.json +59 -0
  14. package/packages/gatsaeng-os/postcss.config.mjs +7 -0
  15. package/packages/gatsaeng-os/public/file.svg +1 -0
  16. package/packages/gatsaeng-os/public/globe.svg +1 -0
  17. package/packages/gatsaeng-os/public/next.svg +1 -0
  18. package/packages/gatsaeng-os/public/vercel.svg +1 -0
  19. package/packages/gatsaeng-os/public/window.svg +1 -0
  20. package/packages/gatsaeng-os/python/api_server.py +248 -0
  21. package/packages/gatsaeng-os/python/briefing.py +145 -0
  22. package/packages/gatsaeng-os/python/config.py +55 -0
  23. package/packages/gatsaeng-os/python/goal_context_agent.py +193 -0
  24. package/packages/gatsaeng-os/python/gyeokguk.py +171 -0
  25. package/packages/gatsaeng-os/python/proactive.py +158 -0
  26. package/packages/gatsaeng-os/python/requirements.txt +11 -0
  27. package/packages/gatsaeng-os/python/run.py +28 -0
  28. package/packages/gatsaeng-os/python/scoring.py +44 -0
  29. package/packages/gatsaeng-os/python/streak.py +70 -0
  30. package/packages/gatsaeng-os/python/telegram_bot.py +331 -0
  31. package/packages/gatsaeng-os/python/timing_engine.py +117 -0
  32. package/packages/gatsaeng-os/python/vault_io.py +423 -0
  33. package/packages/gatsaeng-os/src/app/(dashboard)/areas/[id]/page.tsx +215 -0
  34. package/packages/gatsaeng-os/src/app/(dashboard)/areas/page.tsx +161 -0
  35. package/packages/gatsaeng-os/src/app/(dashboard)/books/[id]/page.tsx +215 -0
  36. package/packages/gatsaeng-os/src/app/(dashboard)/books/page.tsx +268 -0
  37. package/packages/gatsaeng-os/src/app/(dashboard)/calendar/page.tsx +379 -0
  38. package/packages/gatsaeng-os/src/app/(dashboard)/error.tsx +30 -0
  39. package/packages/gatsaeng-os/src/app/(dashboard)/focus/page.tsx +293 -0
  40. package/packages/gatsaeng-os/src/app/(dashboard)/goals/[id]/page.tsx +426 -0
  41. package/packages/gatsaeng-os/src/app/(dashboard)/goals/page.tsx +178 -0
  42. package/packages/gatsaeng-os/src/app/(dashboard)/layout.tsx +29 -0
  43. package/packages/gatsaeng-os/src/app/(dashboard)/notes/[id]/page.tsx +147 -0
  44. package/packages/gatsaeng-os/src/app/(dashboard)/notes/page.tsx +254 -0
  45. package/packages/gatsaeng-os/src/app/(dashboard)/page.tsx +26 -0
  46. package/packages/gatsaeng-os/src/app/(dashboard)/projects/[id]/page.tsx +86 -0
  47. package/packages/gatsaeng-os/src/app/(dashboard)/projects/page.tsx +215 -0
  48. package/packages/gatsaeng-os/src/app/(dashboard)/review/page.tsx +475 -0
  49. package/packages/gatsaeng-os/src/app/(dashboard)/routines/page.tsx +436 -0
  50. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/[id]/page.tsx +210 -0
  51. package/packages/gatsaeng-os/src/app/(dashboard)/tasks/page.tsx +307 -0
  52. package/packages/gatsaeng-os/src/app/(dashboard)/voice/page.tsx +212 -0
  53. package/packages/gatsaeng-os/src/app/api/areas/[id]/route.ts +26 -0
  54. package/packages/gatsaeng-os/src/app/api/areas/route.ts +22 -0
  55. package/packages/gatsaeng-os/src/app/api/auth/login/route.ts +52 -0
  56. package/packages/gatsaeng-os/src/app/api/auth/logout/route.ts +8 -0
  57. package/packages/gatsaeng-os/src/app/api/books/[id]/route.ts +27 -0
  58. package/packages/gatsaeng-os/src/app/api/books/route.ts +20 -0
  59. package/packages/gatsaeng-os/src/app/api/calendar/[id]/route.ts +24 -0
  60. package/packages/gatsaeng-os/src/app/api/calendar/import/route.ts +52 -0
  61. package/packages/gatsaeng-os/src/app/api/calendar/route.ts +37 -0
  62. package/packages/gatsaeng-os/src/app/api/daily/route.ts +51 -0
  63. package/packages/gatsaeng-os/src/app/api/goals/[id]/route.ts +34 -0
  64. package/packages/gatsaeng-os/src/app/api/goals/route.ts +30 -0
  65. package/packages/gatsaeng-os/src/app/api/logs/energy/route.ts +40 -0
  66. package/packages/gatsaeng-os/src/app/api/logs/focus/route.ts +22 -0
  67. package/packages/gatsaeng-os/src/app/api/logs/routine/route.ts +54 -0
  68. package/packages/gatsaeng-os/src/app/api/milestones/[id]/route.ts +26 -0
  69. package/packages/gatsaeng-os/src/app/api/milestones/route.ts +47 -0
  70. package/packages/gatsaeng-os/src/app/api/notes/[id]/route.ts +29 -0
  71. package/packages/gatsaeng-os/src/app/api/notes/route.ts +37 -0
  72. package/packages/gatsaeng-os/src/app/api/profile/route.ts +17 -0
  73. package/packages/gatsaeng-os/src/app/api/projects/[id]/route.ts +27 -0
  74. package/packages/gatsaeng-os/src/app/api/projects/route.ts +25 -0
  75. package/packages/gatsaeng-os/src/app/api/reviews/[id]/route.ts +26 -0
  76. package/packages/gatsaeng-os/src/app/api/reviews/route.ts +29 -0
  77. package/packages/gatsaeng-os/src/app/api/routines/[id]/route.ts +26 -0
  78. package/packages/gatsaeng-os/src/app/api/routines/route.ts +28 -0
  79. package/packages/gatsaeng-os/src/app/api/tasks/[id]/route.ts +28 -0
  80. package/packages/gatsaeng-os/src/app/api/tasks/route.ts +66 -0
  81. package/packages/gatsaeng-os/src/app/api/timing/current/route.ts +63 -0
  82. package/packages/gatsaeng-os/src/app/api/voice/chat/route.ts +50 -0
  83. package/packages/gatsaeng-os/src/app/api/voice/transcribe/route.ts +25 -0
  84. package/packages/gatsaeng-os/src/app/api/voice/tts/route.ts +36 -0
  85. package/packages/gatsaeng-os/src/app/error.tsx +30 -0
  86. package/packages/gatsaeng-os/src/app/favicon.ico +0 -0
  87. package/packages/gatsaeng-os/src/app/globals.css +208 -0
  88. package/packages/gatsaeng-os/src/app/layout.tsx +33 -0
  89. package/packages/gatsaeng-os/src/app/login/page.tsx +87 -0
  90. package/packages/gatsaeng-os/src/app/providers.tsx +27 -0
  91. package/packages/gatsaeng-os/src/components/ErrorBoundary.tsx +46 -0
  92. package/packages/gatsaeng-os/src/components/dashboard/DashboardGrid.tsx +86 -0
  93. package/packages/gatsaeng-os/src/components/dashboard/DdayWidget.tsx +88 -0
  94. package/packages/gatsaeng-os/src/components/dashboard/EnergyTracker.tsx +87 -0
  95. package/packages/gatsaeng-os/src/components/dashboard/FocusTimer.tsx +139 -0
  96. package/packages/gatsaeng-os/src/components/dashboard/GatsaengScore.tsx +30 -0
  97. package/packages/gatsaeng-os/src/components/dashboard/GoalRings.tsx +107 -0
  98. package/packages/gatsaeng-os/src/components/dashboard/ProactiveBar.tsx +98 -0
  99. package/packages/gatsaeng-os/src/components/dashboard/RoutineChecklist.tsx +81 -0
  100. package/packages/gatsaeng-os/src/components/dashboard/TimingWidget.tsx +86 -0
  101. package/packages/gatsaeng-os/src/components/dashboard/WidgetCustomizer.tsx +95 -0
  102. package/packages/gatsaeng-os/src/components/dashboard/WidgetWrapper.tsx +33 -0
  103. package/packages/gatsaeng-os/src/components/dashboard/ZeigarnikPanel.tsx +43 -0
  104. package/packages/gatsaeng-os/src/components/editor/EditorToolbar.tsx +186 -0
  105. package/packages/gatsaeng-os/src/components/editor/TiptapEditor.tsx +114 -0
  106. package/packages/gatsaeng-os/src/components/layout/Header.tsx +47 -0
  107. package/packages/gatsaeng-os/src/components/layout/MobileBottomNav.tsx +122 -0
  108. package/packages/gatsaeng-os/src/components/layout/MobileSidebar.tsx +29 -0
  109. package/packages/gatsaeng-os/src/components/layout/Sidebar.tsx +142 -0
  110. package/packages/gatsaeng-os/src/components/onboarding/OnboardingFlow.tsx +229 -0
  111. package/packages/gatsaeng-os/src/components/onboarding/OnboardingGate.tsx +78 -0
  112. package/packages/gatsaeng-os/src/components/projects/CalendarView.tsx +152 -0
  113. package/packages/gatsaeng-os/src/components/projects/KanbanView.tsx +180 -0
  114. package/packages/gatsaeng-os/src/components/projects/ListView.tsx +82 -0
  115. package/packages/gatsaeng-os/src/components/projects/TableView.tsx +206 -0
  116. package/packages/gatsaeng-os/src/components/projects/TaskCard.tsx +154 -0
  117. package/packages/gatsaeng-os/src/components/projects/TaskForm.tsx +128 -0
  118. package/packages/gatsaeng-os/src/components/projects/ViewSwitcher.tsx +40 -0
  119. package/packages/gatsaeng-os/src/components/search/GlobalSearch.tsx +179 -0
  120. package/packages/gatsaeng-os/src/components/shared/InlineEdit.tsx +77 -0
  121. package/packages/gatsaeng-os/src/components/shared/PinButton.tsx +42 -0
  122. package/packages/gatsaeng-os/src/components/tasks/DDayBadge.tsx +34 -0
  123. package/packages/gatsaeng-os/src/components/ui/badge.tsx +48 -0
  124. package/packages/gatsaeng-os/src/components/ui/button.tsx +64 -0
  125. package/packages/gatsaeng-os/src/components/ui/card.tsx +92 -0
  126. package/packages/gatsaeng-os/src/components/ui/checkbox.tsx +32 -0
  127. package/packages/gatsaeng-os/src/components/ui/command.tsx +184 -0
  128. package/packages/gatsaeng-os/src/components/ui/dialog.tsx +158 -0
  129. package/packages/gatsaeng-os/src/components/ui/input.tsx +21 -0
  130. package/packages/gatsaeng-os/src/components/ui/label.tsx +24 -0
  131. package/packages/gatsaeng-os/src/components/ui/popover.tsx +89 -0
  132. package/packages/gatsaeng-os/src/components/ui/progress.tsx +31 -0
  133. package/packages/gatsaeng-os/src/components/ui/select.tsx +190 -0
  134. package/packages/gatsaeng-os/src/components/ui/sheet.tsx +143 -0
  135. package/packages/gatsaeng-os/src/components/ui/tabs.tsx +91 -0
  136. package/packages/gatsaeng-os/src/components/ui/toggle-group.tsx +83 -0
  137. package/packages/gatsaeng-os/src/components/ui/toggle.tsx +47 -0
  138. package/packages/gatsaeng-os/src/components/ui/tooltip.tsx +57 -0
  139. package/packages/gatsaeng-os/src/hooks/useAreas.ts +53 -0
  140. package/packages/gatsaeng-os/src/hooks/useBooks.ts +62 -0
  141. package/packages/gatsaeng-os/src/hooks/useCalendar.ts +59 -0
  142. package/packages/gatsaeng-os/src/hooks/useDaily.ts +15 -0
  143. package/packages/gatsaeng-os/src/hooks/useGlobalTasks.ts +45 -0
  144. package/packages/gatsaeng-os/src/hooks/useGoals.ts +53 -0
  145. package/packages/gatsaeng-os/src/hooks/useMilestones.ts +75 -0
  146. package/packages/gatsaeng-os/src/hooks/useNotes.ts +65 -0
  147. package/packages/gatsaeng-os/src/hooks/useProjects.ts +102 -0
  148. package/packages/gatsaeng-os/src/hooks/useRoutines.ts +76 -0
  149. package/packages/gatsaeng-os/src/hooks/useTiming.ts +27 -0
  150. package/packages/gatsaeng-os/src/lib/apiFetch.ts +14 -0
  151. package/packages/gatsaeng-os/src/lib/auth.ts +32 -0
  152. package/packages/gatsaeng-os/src/lib/date.ts +7 -0
  153. package/packages/gatsaeng-os/src/lib/editor/markdown.ts +35 -0
  154. package/packages/gatsaeng-os/src/lib/llm-governor.ts +167 -0
  155. package/packages/gatsaeng-os/src/lib/neuroscience/energyCycle.ts +35 -0
  156. package/packages/gatsaeng-os/src/lib/neuroscience/habitStack.ts +22 -0
  157. package/packages/gatsaeng-os/src/lib/neuroscience/scoring.ts +32 -0
  158. package/packages/gatsaeng-os/src/lib/routes.ts +15 -0
  159. package/packages/gatsaeng-os/src/lib/utils.ts +6 -0
  160. package/packages/gatsaeng-os/src/lib/vault/config.ts +29 -0
  161. package/packages/gatsaeng-os/src/lib/vault/frontmatter.ts +84 -0
  162. package/packages/gatsaeng-os/src/lib/vault/index.ts +180 -0
  163. package/packages/gatsaeng-os/src/lib/vault/schemas.ts +274 -0
  164. package/packages/gatsaeng-os/src/middleware.ts +34 -0
  165. package/packages/gatsaeng-os/src/stores/dashboardStore.ts +26 -0
  166. package/packages/gatsaeng-os/src/stores/favoritesStore.ts +47 -0
  167. package/packages/gatsaeng-os/src/stores/timerStore.ts +65 -0
  168. package/packages/gatsaeng-os/src/types/index.ts +320 -0
  169. package/packages/gatsaeng-os/tsconfig.json +34 -0
  170. package/templates/scripts/forge_qa.sh.tmpl +237 -0
  171. package/templates/scripts/forge_ship.sh.tmpl +183 -0
  172. package/templates/scripts/session_indexer.py.tmpl +420 -0
  173. package/templates/scripts/tracer.py.tmpl +266 -0
  174. package/templates/workspace/AGENTS.md.tmpl +190 -0
  175. package/templates/workspace/BOOTSTRAP.md.tmpl +27 -0
  176. package/templates/workspace/HEARTBEAT.md.tmpl +23 -0
  177. package/templates/workspace/MEMORY.md.tmpl +35 -0
  178. package/templates/workspace/SOUL.md.tmpl +258 -0
  179. package/templates/workspace/TOOLS.md.tmpl +28 -0
  180. package/templates/workspace/USER.md.tmpl +43 -0
@@ -0,0 +1,117 @@
1
+ """사주 Timing Engine — 사전 계산된 월운 데이터 기반 타이밍 컨텍스트 제공."""
2
+
3
+ from datetime import date, timedelta
4
+ from typing import Optional
5
+
6
+ import vault_io
7
+
8
+
9
+ def get_current_context() -> Optional[dict]:
10
+ """현재 월운 컨텍스트를 반환."""
11
+ return vault_io.get_current_timing()
12
+
13
+
14
+ def get_monthly_briefing() -> str:
15
+ """월운 기반 모닝 브리핑 컨텍스트 생성."""
16
+ timing = get_current_context()
17
+ if not timing:
18
+ return ''
19
+
20
+ rating = timing.get('rating', 3)
21
+ parts = []
22
+
23
+ # Rating-based frame
24
+ if rating >= 4:
25
+ parts.append(f"이번 달({timing.get('pillar', '')})은 적극적으로 움직일 타이밍이야.")
26
+ elif rating == 3:
27
+ parts.append(f"이번 달({timing.get('pillar', '')})은 기존 루틴 유지에 집중.")
28
+ else:
29
+ parts.append(f"이번 달({timing.get('pillar', '')})은 신중하게. 무리한 확장은 보류.")
30
+
31
+ # Theme
32
+ if timing.get('theme'):
33
+ parts.append(f"테마: {timing['theme']}")
34
+
35
+ # Action guide
36
+ guides = timing.get('action_guide', [])
37
+ if guides:
38
+ parts.append('실행 가이드:')
39
+ for g in guides[:3]:
40
+ parts.append(f' - {g}')
41
+
42
+ # Caution
43
+ cautions = timing.get('caution', [])
44
+ if cautions:
45
+ parts.append('주의:')
46
+ for c in cautions[:2]:
47
+ parts.append(f' - {c}')
48
+
49
+ return '\n'.join(parts)
50
+
51
+
52
+ def get_timing_score_modifier(event_type: str) -> float:
53
+ """운세에 따른 점수 보정 계수 (0.8 ~ 1.2)."""
54
+ timing = get_current_context()
55
+ if not timing:
56
+ return 1.0
57
+
58
+ rating = timing.get('rating', 3)
59
+
60
+ # 유리한 달에는 보너스, 불리한 달에는 감소 없음 (벌점은 주지 않음)
61
+ if rating >= 4:
62
+ return 1.1 # 10% 보너스
63
+ elif rating == 5:
64
+ return 1.2 # 20% 보너스
65
+
66
+ return 1.0
67
+
68
+
69
+ def get_goal_timing_advice(goal_id: str) -> str:
70
+ """특정 목표에 대한 타이밍 기반 조언."""
71
+ timing = get_current_context()
72
+ goal = vault_io.read_goal(goal_id)
73
+
74
+ if not timing or not goal:
75
+ return ''
76
+
77
+ rating = timing.get('rating', 3)
78
+ theme = timing.get('theme', '')
79
+ goal_type = goal['data'].get('type', '')
80
+ goal_title = goal['data'].get('title', '')
81
+
82
+ advice = []
83
+
84
+ if rating >= 4:
85
+ advice.append(f"'{goal_title}' — 이 달은 확장에 유리. 마일스톤 달성 속도를 높여라.")
86
+ if 'networking' in theme.lower() or '인맥' in theme or '관계' in theme:
87
+ advice.append("특히 사람을 통한 기회가 열리는 시기. 협업 제안 적극적으로.")
88
+ elif rating <= 2:
89
+ advice.append(f"'{goal_title}' — 이 달은 기초 다지기에 집중. 새로운 시작보다 완성.")
90
+ advice.append("무리한 목표 수정보다 기존 루틴 유지가 더 효과적.")
91
+ else:
92
+ advice.append(f"'{goal_title}' — 균형 잡힌 시기. 현재 페이스 유지하면서 미세 조정.")
93
+
94
+ return '\n'.join(advice)
95
+
96
+
97
+ def should_trigger_reanalysis(goal_id: str) -> bool:
98
+ """목표 재분석이 필요한지 판단."""
99
+ goal = vault_io.read_goal(goal_id)
100
+ if not goal:
101
+ return False
102
+
103
+ next_review = goal['data'].get('ai_next_review')
104
+ if not next_review:
105
+ return True # 한 번도 분석 안 함
106
+
107
+ try:
108
+ review_date = date.fromisoformat(str(next_review)[:10])
109
+ return date.today() >= review_date
110
+ except ValueError:
111
+ return True
112
+
113
+
114
+ def get_all_due_reanalyses() -> list[str]:
115
+ """재분석이 필요한 모든 목표 ID 반환."""
116
+ goals = vault_io.get_active_goals()
117
+ return [g['id'] for g in goals if should_trigger_reanalysis(g['id'])]
@@ -0,0 +1,423 @@
1
+ """Obsidian Vault I/O — Python mirror of Node.js vault/index.ts"""
2
+
3
+ import frontmatter
4
+ from pathlib import Path
5
+ from datetime import date, datetime, timedelta
6
+ from typing import Optional, Any
7
+ import yaml
8
+ import uuid
9
+
10
+ from config import FOLDERS, PROFILE_PATH, VAULT_PATH
11
+
12
+
13
+ def _ensure_dir(folder: str) -> Path:
14
+ path = FOLDERS[folder]
15
+ path.mkdir(parents=True, exist_ok=True)
16
+ return path
17
+
18
+
19
+ def _gen_id() -> str:
20
+ return uuid.uuid4().hex[:10]
21
+
22
+
23
+ # ── Read Operations ──
24
+
25
+ def list_entities(folder: str) -> list[dict[str, Any]]:
26
+ """List all .md files in a folder, parse frontmatter."""
27
+ path = _ensure_dir(folder)
28
+ results = []
29
+ for f in sorted(path.glob('*.md')):
30
+ post = frontmatter.load(str(f))
31
+ results.append({
32
+ 'data': dict(post.metadata),
33
+ 'content': post.content.strip(),
34
+ 'filename': f.name,
35
+ })
36
+ return results
37
+
38
+
39
+ def get_entity(folder: str, entity_id: str) -> Optional[dict[str, Any]]:
40
+ """Get a single entity by ID (searches filename)."""
41
+ path = FOLDERS[folder]
42
+ if not path.exists():
43
+ return None
44
+ for f in path.glob('*.md'):
45
+ if entity_id in f.name:
46
+ post = frontmatter.load(str(f))
47
+ return {
48
+ 'data': dict(post.metadata),
49
+ 'content': post.content.strip(),
50
+ }
51
+ return None
52
+
53
+
54
+ def get_entity_by_date(folder: str, date_str: str) -> Optional[dict[str, Any]]:
55
+ """Get a date-based entity (e.g., tasks/2026-03-05.md)."""
56
+ path = FOLDERS[folder] / f'{date_str}.md'
57
+ if not path.exists():
58
+ return None
59
+ post = frontmatter.load(str(path))
60
+ return {
61
+ 'data': dict(post.metadata),
62
+ 'content': post.content.strip(),
63
+ }
64
+
65
+
66
+ def read_today_tasks() -> dict[str, Any]:
67
+ """Read today's daily manifest."""
68
+ today = date.today().isoformat()
69
+ result = get_entity_by_date('tasks', today)
70
+ if result:
71
+ return result
72
+ return {
73
+ 'data': {
74
+ 'date': today,
75
+ 'gatsaeng_score': 0,
76
+ 'routines_done': 0,
77
+ 'routines_total': 0,
78
+ 'focus_minutes': 0,
79
+ },
80
+ 'content': '',
81
+ }
82
+
83
+
84
+ def read_goal(goal_id: str) -> Optional[dict[str, Any]]:
85
+ return get_entity('goals', goal_id)
86
+
87
+
88
+ def get_routines_with_status() -> list[dict[str, Any]]:
89
+ """Get all active routines with today's completion status."""
90
+ routines = list_entities('routines')
91
+ today = date.today().isoformat()
92
+ today_log = get_entity_by_date('routine_logs', today)
93
+
94
+ completed_ids = set()
95
+ if today_log and 'completions' in today_log['data']:
96
+ for c in today_log['data']['completions']:
97
+ completed_ids.add(c.get('routine_id', ''))
98
+
99
+ result = []
100
+ for r in routines:
101
+ d = r['data']
102
+ if d.get('is_active', True):
103
+ d['completed_today'] = d.get('id', '') in completed_ids
104
+ result.append(d)
105
+ return result
106
+
107
+
108
+ def get_active_goals() -> list[dict[str, Any]]:
109
+ """Get all active goals."""
110
+ goals = list_entities('goals')
111
+ return [g['data'] for g in goals if g['data'].get('status') == 'active']
112
+
113
+
114
+ def get_active_milestones() -> list[dict[str, Any]]:
115
+ """Get all active milestones with D-day calculation."""
116
+ milestones = list_entities('milestones')
117
+ today = date.today()
118
+ result = []
119
+ for m in milestones:
120
+ d = m['data']
121
+ if d.get('status') == 'active' and d.get('due_date'):
122
+ due = date.fromisoformat(str(d['due_date'])[:10])
123
+ d['d_day'] = (due - today).days
124
+ result.append(d)
125
+ return sorted(result, key=lambda x: x.get('d_day', 999))
126
+
127
+
128
+ def get_goals_with_milestones() -> list[dict[str, Any]]:
129
+ """Get goals with their milestones and D-day info."""
130
+ goals = get_active_goals()
131
+ milestones = get_active_milestones()
132
+
133
+ for goal in goals:
134
+ goal_id = goal.get('id', '')
135
+ goal['milestones'] = [m for m in milestones if m.get('goal_id') == goal_id]
136
+ return goals
137
+
138
+
139
+ def get_current_timing() -> Optional[dict[str, Any]]:
140
+ """Get the current month's timing context."""
141
+ today = date.today()
142
+ timings = list_entities('timing')
143
+ for t in timings:
144
+ d = t['data']
145
+ start = d.get('period_start', '')
146
+ end = d.get('period_end', '')
147
+ if start and end:
148
+ start_date = date.fromisoformat(str(start)[:10])
149
+ end_date = date.fromisoformat(str(end)[:10])
150
+ if start_date <= today <= end_date:
151
+ return d
152
+ return None
153
+
154
+
155
+ def get_active_streaks() -> dict[str, Any]:
156
+ """Get current and longest streaks from profile."""
157
+ profile = read_profile()
158
+ return {
159
+ 'current': profile.get('current_streak', 0),
160
+ 'longest': profile.get('longest_streak', 0),
161
+ }
162
+
163
+
164
+ def get_today_score() -> int:
165
+ """Get today's gatsaeng score."""
166
+ today_data = read_today_tasks()
167
+ return today_data['data'].get('gatsaeng_score', 0)
168
+
169
+
170
+ def get_yesterday_score() -> int:
171
+ """Get yesterday's gatsaeng score."""
172
+ yesterday = (date.today() - timedelta(days=1)).isoformat()
173
+ result = get_entity_by_date('tasks', yesterday)
174
+ if result:
175
+ return result['data'].get('gatsaeng_score', 0)
176
+ return 0
177
+
178
+
179
+ def get_unchecked_today() -> list[dict[str, Any]]:
180
+ """Get routines and tasks not yet completed today."""
181
+ routines = get_routines_with_status()
182
+ unchecked = [r for r in routines if not r.get('completed_today', False)]
183
+ return unchecked
184
+
185
+
186
+ def get_consecutive_skips() -> dict[str, int]:
187
+ """Check consecutive days routines were skipped."""
188
+ routines = list_entities('routines')
189
+ active_routines = {r['data']['id']: r['data'] for r in routines if r['data'].get('is_active')}
190
+
191
+ skips: dict[str, int] = {rid: 0 for rid in active_routines}
192
+ today = date.today()
193
+
194
+ for days_ago in range(1, 8): # check last 7 days
195
+ check_date = (today - timedelta(days=days_ago)).isoformat()
196
+ log = get_entity_by_date('routine_logs', check_date)
197
+ completed_ids = set()
198
+ if log and 'completions' in log['data']:
199
+ for c in log['data']['completions']:
200
+ completed_ids.add(c.get('routine_id', ''))
201
+
202
+ for rid in active_routines:
203
+ r = active_routines[rid]
204
+ day_of_week = (today - timedelta(days=days_ago)).isoweekday()
205
+ scheduled = r.get('scheduled_days', [1,2,3,4,5,6,7])
206
+ if day_of_week in scheduled:
207
+ if rid not in completed_ids:
208
+ skips[rid] += 1
209
+ else:
210
+ break # streak broken, stop counting
211
+
212
+ return {rid: count for rid, count in skips.items() if count > 0}
213
+
214
+
215
+ def get_upcoming_ddays(days: int = 90) -> list[dict[str, Any]]:
216
+ """Get milestones with D-day within range."""
217
+ milestones = get_active_milestones()
218
+ return [m for m in milestones if 0 < m.get('d_day', 999) <= days]
219
+
220
+
221
+ def read_profile() -> dict[str, Any]:
222
+ """Read user profile."""
223
+ if not PROFILE_PATH.exists():
224
+ return {
225
+ 'display_name': 'Drake',
226
+ 'level': 1,
227
+ 'total_score': 0,
228
+ 'longest_streak': 0,
229
+ 'current_streak': 0,
230
+ 'peak_hours': [9, 10, 11],
231
+ }
232
+ post = frontmatter.load(str(PROFILE_PATH))
233
+ return dict(post.metadata)
234
+
235
+
236
+ def read_last_review() -> Optional[dict[str, Any]]:
237
+ """Read the most recent weekly review."""
238
+ reviews = list_entities('reviews')
239
+ weekly = [r for r in reviews if 'weekly' in r['filename'] or r['data'].get('type') == 'weekly']
240
+ if not weekly:
241
+ return None
242
+ weekly.sort(key=lambda x: x['data'].get('week_start', ''), reverse=True)
243
+ return weekly[0]
244
+
245
+
246
+ def read_context_data(goal_id: str) -> list[dict[str, Any]]:
247
+ """Read context-data files for a goal."""
248
+ context_dir = FOLDERS['goals'] / goal_id / 'context-data'
249
+ if not context_dir.exists():
250
+ return []
251
+ files = []
252
+ for f in context_dir.iterdir():
253
+ files.append({
254
+ 'name': f.name,
255
+ 'path': str(f),
256
+ 'size': f.stat().st_size,
257
+ 'suffix': f.suffix,
258
+ })
259
+ return files
260
+
261
+
262
+ def read_recent_tasks(days: int = 14) -> list[dict[str, Any]]:
263
+ """Read task manifests for recent days."""
264
+ today = date.today()
265
+ results = []
266
+ for i in range(days):
267
+ d = (today - timedelta(days=i)).isoformat()
268
+ result = get_entity_by_date('tasks', d)
269
+ if result:
270
+ results.append(result)
271
+ return results
272
+
273
+
274
+ # ── Write Operations ──
275
+
276
+ def create_entity(folder: str, data: dict[str, Any], body: str = '') -> dict[str, Any]:
277
+ """Create a new entity file."""
278
+ path = _ensure_dir(folder)
279
+ entity_id = data.get('id') or _gen_id()
280
+ data['id'] = entity_id
281
+
282
+ prefix = folder.rstrip('s')
283
+ filename = f'{prefix}-{entity_id}.md'
284
+
285
+ post = frontmatter.Post(body, **data)
286
+ filepath = path / filename
287
+ filepath.write_text(frontmatter.dumps(post), encoding='utf-8')
288
+
289
+ return data
290
+
291
+
292
+ def create_date_entity(folder: str, date_str: str, data: dict[str, Any], body: str = '') -> dict[str, Any]:
293
+ """Create a date-based entity file."""
294
+ path = _ensure_dir(folder)
295
+ post = frontmatter.Post(body, **data)
296
+ filepath = path / f'{date_str}.md'
297
+ filepath.write_text(frontmatter.dumps(post), encoding='utf-8')
298
+ return data
299
+
300
+
301
+ def update_entity(folder: str, entity_id: str, updates: dict[str, Any], body: Optional[str] = None) -> Optional[dict[str, Any]]:
302
+ """Update an entity's frontmatter."""
303
+ path = FOLDERS[folder]
304
+ for f in path.glob('*.md'):
305
+ if entity_id in f.name:
306
+ post = frontmatter.load(str(f))
307
+ for k, v in updates.items():
308
+ post.metadata[k] = v
309
+ if body is not None:
310
+ post.content = body
311
+ f.write_text(frontmatter.dumps(post), encoding='utf-8')
312
+ return dict(post.metadata)
313
+ return None
314
+
315
+
316
+ def check_routine(routine_id: str) -> dict[str, Any]:
317
+ """Mark a routine as completed today."""
318
+ today = date.today().isoformat()
319
+ now = datetime.now().isoformat()
320
+
321
+ log = get_entity_by_date('routine_logs', today)
322
+
323
+ if log:
324
+ completions = log['data'].get('completions', [])
325
+ # check if already completed
326
+ if any(c.get('routine_id') == routine_id for c in completions):
327
+ return log['data']
328
+ completions.append({
329
+ 'routine_id': routine_id,
330
+ 'completed_at': now,
331
+ })
332
+ log['data']['completions'] = completions
333
+ create_date_entity('routine_logs', today, log['data'])
334
+ else:
335
+ data = {
336
+ 'date': today,
337
+ 'completions': [{
338
+ 'routine_id': routine_id,
339
+ 'completed_at': now,
340
+ }],
341
+ }
342
+ create_date_entity('routine_logs', today, data)
343
+ log = {'data': data}
344
+
345
+ # update streak
346
+ routine = get_entity('routines', routine_id)
347
+ if routine:
348
+ streak = routine['data'].get('streak', 0) + 1
349
+ longest = max(routine['data'].get('longest_streak', 0), streak)
350
+ update_entity('routines', routine_id, {
351
+ 'streak': streak,
352
+ 'longest_streak': longest,
353
+ })
354
+
355
+ return log['data']
356
+
357
+
358
+ def update_today_score(delta: int) -> int:
359
+ """Add points to today's score."""
360
+ today = date.today().isoformat()
361
+ manifest = read_today_tasks()
362
+ new_score = manifest['data'].get('gatsaeng_score', 0) + delta
363
+ manifest['data']['gatsaeng_score'] = new_score
364
+ create_date_entity('tasks', today, manifest['data'], manifest.get('content', ''))
365
+
366
+ # update profile total
367
+ profile = read_profile()
368
+ profile['total_score'] = profile.get('total_score', 0) + delta
369
+ update_profile(profile)
370
+
371
+ return new_score
372
+
373
+
374
+ def update_profile(updates: dict[str, Any]) -> dict[str, Any]:
375
+ """Update user profile."""
376
+ if PROFILE_PATH.exists():
377
+ post = frontmatter.load(str(PROFILE_PATH))
378
+ for k, v in updates.items():
379
+ post.metadata[k] = v
380
+ PROFILE_PATH.write_text(frontmatter.dumps(post), encoding='utf-8')
381
+ return dict(post.metadata)
382
+ else:
383
+ post = frontmatter.Post('', **updates)
384
+ PROFILE_PATH.parent.mkdir(parents=True, exist_ok=True)
385
+ PROFILE_PATH.write_text(frontmatter.dumps(post), encoding='utf-8')
386
+ return updates
387
+
388
+
389
+ def save_context_file(goal_id: str, filename: str, content: bytes) -> Path:
390
+ """Save a context-data file for a goal."""
391
+ context_dir = FOLDERS['goals'] / goal_id / 'context-data'
392
+ context_dir.mkdir(parents=True, exist_ok=True)
393
+ filepath = context_dir / filename
394
+ filepath.write_bytes(content)
395
+ return filepath
396
+
397
+
398
+ def get_heatmap(weeks: int = 52) -> list[dict[str, Any]]:
399
+ """Get completion data for heatmap (last N weeks)."""
400
+ today = date.today()
401
+ data = []
402
+ for i in range(weeks * 7):
403
+ d = today - timedelta(days=i)
404
+ d_str = d.isoformat()
405
+ log = get_entity_by_date('routine_logs', d_str)
406
+ manifest = get_entity_by_date('tasks', d_str)
407
+
408
+ completions = 0
409
+ if log and 'completions' in log['data']:
410
+ completions = len(log['data']['completions'])
411
+
412
+ score = 0
413
+ if manifest:
414
+ score = manifest['data'].get('gatsaeng_score', 0)
415
+
416
+ data.append({
417
+ 'date': d_str,
418
+ 'completions': completions,
419
+ 'score': score,
420
+ 'day_of_week': d.isoweekday(),
421
+ })
422
+
423
+ return list(reversed(data))