mulmoclaude 0.6.2 → 0.6.4

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 (182) hide show
  1. package/README.md +26 -0
  2. package/bin/mulmoclaude.js +11 -1
  3. package/client/assets/JsonEditor-D6WBWLoa.js +10 -0
  4. package/client/assets/JsonEditor-Di5xGeZY.css +1 -0
  5. package/client/assets/_plugin-vue_export-helper-BOai-rQB.js +1 -0
  6. package/client/assets/chunk-D8eiyYIV-LcKZGJv5.js +1 -0
  7. package/client/assets/{html2canvas-CDGcmOD3-Bkf2uOth.js → html2canvas-CDGcmOD3-XVrO-eyz.js} +1 -1
  8. package/client/assets/index-CyBr8Mkr.css +2 -0
  9. package/client/assets/index-zZIqEbNX.js +5106 -0
  10. package/client/assets/{index.es-DqtpmBm8-D9mAh_KQ.js → index.es-DqtpmBm8-DHT6q10o.js} +1 -1
  11. package/client/assets/material-symbols-outlined-DtIK7AQn.woff2 +0 -0
  12. package/client/assets/runtime-protocol-vue-D6kcV0wa.js +1 -0
  13. package/client/assets/{runtime-vue-BVUzgYGA.js → runtime-vue-fFYhnNg3.js} +1 -1
  14. package/client/assets/{vue-C8UuIO9J.js → vue-D4w8THF_.js} +1 -1
  15. package/client/assets/vue-i18n-CQbxVmNs.js +3 -0
  16. package/client/assets/vue.runtime.esm-bundler-BTyIdNAI.js +4 -0
  17. package/client/index.html +10 -10
  18. package/package.json +9 -8
  19. package/server/agent/backend/claude-code.ts +34 -0
  20. package/server/agent/backend/fake-echo.ts +370 -0
  21. package/server/agent/backend/index.ts +16 -1
  22. package/server/agent/config.ts +74 -24
  23. package/server/agent/index.ts +104 -80
  24. package/server/agent/mcpFailureMonitor.ts +167 -0
  25. package/server/agent/mcpPreflight.ts +185 -0
  26. package/server/agent/prompt.ts +50 -359
  27. package/server/agent/stdioHttpShim.ts +171 -0
  28. package/server/agent/stream.ts +12 -1
  29. package/server/api/routes/encore.ts +55 -0
  30. package/server/api/routes/files.ts +22 -0
  31. package/server/api/routes/mulmo-script.ts +19 -1
  32. package/server/api/routes/schedulerHandlers.ts +52 -4
  33. package/server/api/routes/sessions.ts +15 -0
  34. package/server/api/routes/skills.ts +263 -0
  35. package/server/build/dispatcher.mjs +299 -0
  36. package/server/encore/INVARIANTS.md +272 -0
  37. package/server/encore/boot.ts +39 -0
  38. package/server/encore/closure.ts +36 -0
  39. package/server/encore/cycle.ts +276 -0
  40. package/server/encore/dispatch.ts +103 -0
  41. package/server/encore/handlers/amend.ts +99 -0
  42. package/server/encore/handlers/appendNote.ts +74 -0
  43. package/server/encore/handlers/defineEncore.ts +42 -0
  44. package/server/encore/handlers/listTickets.ts +107 -0
  45. package/server/encore/handlers/markStepDone.ts +41 -0
  46. package/server/encore/handlers/markTargetSkipped.ts +33 -0
  47. package/server/encore/handlers/query.ts +138 -0
  48. package/server/encore/handlers/recordValues.ts +44 -0
  49. package/server/encore/handlers/resolveNotification.ts +121 -0
  50. package/server/encore/handlers/setup.ts +81 -0
  51. package/server/encore/handlers/shared.ts +137 -0
  52. package/server/encore/handlers/snooze.ts +87 -0
  53. package/server/encore/handlers/startObligationChat.ts +64 -0
  54. package/server/encore/handlers/startSetupChat.ts +50 -0
  55. package/server/encore/lock.ts +61 -0
  56. package/server/encore/notifier.ts +123 -0
  57. package/server/encore/obligation.ts +25 -0
  58. package/server/encore/paths.ts +78 -0
  59. package/server/encore/reconcile.ts +661 -0
  60. package/server/encore/tick.ts +191 -0
  61. package/server/encore/yaml-fm.ts +63 -0
  62. package/server/events/notifications.ts +19 -91
  63. package/server/index.ts +94 -9
  64. package/server/notifier/engine.ts +102 -1
  65. package/server/notifier/macosReminderAdapter.ts +30 -0
  66. package/server/notifier/runtime-api.ts +41 -1
  67. package/server/notifier/types.ts +15 -2
  68. package/server/plugins/runtime.ts +11 -2
  69. package/server/prompts/index.ts +39 -0
  70. package/server/prompts/system/journal-pointer.md +12 -0
  71. package/server/prompts/system/memory-management-atomic.md +33 -0
  72. package/server/prompts/system/memory-management-topic.md +60 -0
  73. package/server/prompts/system/news-concierge.md +24 -0
  74. package/server/prompts/system/sandbox-tools.md +10 -0
  75. package/server/prompts/system/sources-context.md +16 -0
  76. package/server/prompts/system/system.md +91 -0
  77. package/server/system/announceOptionalDeps.ts +57 -0
  78. package/server/system/appVersion.ts +34 -0
  79. package/server/system/config.ts +17 -1
  80. package/server/system/docker.ts +14 -6
  81. package/server/system/env.ts +18 -5
  82. package/server/system/optionalDeps.ts +129 -0
  83. package/server/utils/cli-flags.d.mts +14 -0
  84. package/server/utils/cli-flags.mjs +53 -0
  85. package/server/utils/files/encore-io.ts +111 -0
  86. package/server/utils/time.ts +6 -0
  87. package/server/workspace/helps/business.md +2 -2
  88. package/server/workspace/helps/encore-dsl.md +482 -0
  89. package/server/workspace/helps/index.md +15 -13
  90. package/server/workspace/helps/mulmoscript.md +3 -3
  91. package/server/workspace/helps/sandbox.md +2 -2
  92. package/server/workspace/hooks/dispatcher.ts +7 -5
  93. package/server/workspace/hooks/provision.ts +6 -3
  94. package/server/workspace/paths.ts +13 -4
  95. package/server/workspace/skills/catalog.ts +355 -0
  96. package/server/workspace/skills/external/catalog.ts +283 -0
  97. package/server/workspace/skills/external/clone.ts +129 -0
  98. package/server/workspace/skills/external/id.ts +194 -0
  99. package/server/workspace/skills/external/install.ts +417 -0
  100. package/server/workspace/skills/external/presets.ts +50 -0
  101. package/server/workspace/skills-preset.ts +29 -17
  102. package/server/workspace/workspace.ts +10 -5
  103. package/src/App.vue +37 -8
  104. package/src/components/FileContentRenderer.vue +102 -9
  105. package/src/components/JsonEditor.vue +160 -0
  106. package/src/components/NotificationBell.vue +35 -3
  107. package/src/components/PluginLauncher.vue +20 -41
  108. package/src/components/RightSidebar.vue +19 -0
  109. package/src/components/SettingsMcpTab.vue +58 -11
  110. package/src/components/SettingsModal.vue +22 -1
  111. package/src/components/StackView.vue +10 -1
  112. package/src/components/TodoExplorer.vue +16 -0
  113. package/src/components/todo/TodoKanbanView.vue +34 -6
  114. package/src/composables/useNotifications.ts +21 -1
  115. package/src/config/apiRoutes.ts +0 -6
  116. package/src/config/mcpCatalog.ts +12 -7
  117. package/src/config/mcpTypes.ts +5 -0
  118. package/src/config/roles.ts +52 -15
  119. package/src/config/systemFileDescriptors.ts +12 -0
  120. package/src/lang/de.ts +108 -12
  121. package/src/lang/en.ts +105 -11
  122. package/src/lang/es.ts +106 -11
  123. package/src/lang/fr.ts +106 -11
  124. package/src/lang/ja.ts +104 -11
  125. package/src/lang/ko.ts +105 -11
  126. package/src/lang/pt-BR.ts +106 -11
  127. package/src/lang/zh.ts +103 -11
  128. package/src/main.ts +1 -0
  129. package/src/plugins/_generated/metas.ts +4 -0
  130. package/src/plugins/_generated/registrations.ts +2 -0
  131. package/src/plugins/_generated/server-bindings.ts +5 -0
  132. package/src/plugins/encore/EncoreDashboard.vue +504 -0
  133. package/src/plugins/encore/EncoreRedirect.vue +116 -0
  134. package/src/plugins/encore/View.vue +36 -0
  135. package/src/plugins/encore/defineEncoreDefinition.ts +74 -0
  136. package/src/plugins/encore/defineEncoreMeta.ts +13 -0
  137. package/src/plugins/encore/index.ts +93 -0
  138. package/src/plugins/encore/manageEncoreDefinition.ts +100 -0
  139. package/src/plugins/encore/manageEncoreMeta.ts +36 -0
  140. package/src/plugins/manageSkills/View.vue +832 -30
  141. package/src/plugins/manageSkills/categories.ts +125 -0
  142. package/src/plugins/manageSkills/meta.ts +30 -0
  143. package/src/plugins/markdown/definition.ts +3 -3
  144. package/src/plugins/meta-types.ts +5 -0
  145. package/src/plugins/presentMulmoScript/Preview.vue +3 -3
  146. package/src/plugins/presentMulmoScript/View.vue +157 -33
  147. package/src/plugins/presentMulmoScript/meta.ts +4 -0
  148. package/src/plugins/scheduler/View.vue +45 -9
  149. package/src/plugins/scheduler/calendarDefinition.ts +6 -2
  150. package/src/plugins/scheduler/multiDayHelpers.ts +95 -0
  151. package/src/plugins/skill/View.vue +1 -5
  152. package/src/plugins/spreadsheet/View.vue +3 -3
  153. package/src/plugins/spreadsheet/definition.ts +1 -1
  154. package/src/plugins/textResponse/Preview.vue +14 -1
  155. package/src/plugins/textResponse/View.vue +39 -24
  156. package/src/plugins/wiki/components/WikiPageBody.vue +4 -0
  157. package/src/router/index.ts +11 -0
  158. package/src/router/pageRoutes.ts +1 -0
  159. package/src/types/encore-dsl/at-expression.ts +120 -0
  160. package/src/types/encore-dsl/at-resolver.ts +32 -0
  161. package/src/types/encore-dsl/cadence.ts +289 -0
  162. package/src/types/encore-dsl/schema.ts +288 -0
  163. package/src/types/notification.ts +2 -1
  164. package/src/types/session.ts +6 -0
  165. package/src/types/sse.ts +5 -0
  166. package/src/types/toolCallHistory.ts +7 -0
  167. package/src/utils/agent/eventDispatch.ts +26 -5
  168. package/src/utils/agent/mcpHint.ts +50 -0
  169. package/src/utils/image/htmlSrcAttrs.ts +117 -13
  170. package/src/utils/session/sessionEntries.ts +8 -32
  171. package/client/assets/PluginScopedRoot-YjvQq0Nn.js +0 -3
  172. package/client/assets/chunk-CernVdwh.js +0 -1
  173. package/client/assets/chunk-D8eiyYIV-CAXpUwLd.js +0 -1
  174. package/client/assets/index-BwrlMMHr.js +0 -5005
  175. package/client/assets/index-CvvNuegU.css +0 -2
  176. package/client/assets/material-symbols-outlined-BOZVWuR3.woff2 +0 -0
  177. package/client/assets/runtime-protocol-vue-C1To4M3t.js +0 -1
  178. package/client/assets/vue.runtime.esm-bundler-DQ8Kjjui.js +0 -4
  179. package/server/api/routes/notifications.ts +0 -195
  180. package/server/notifier/legacy-adapters.ts +0 -76
  181. package/server/workspace/hooks/dispatcher.mjs +0 -300
  182. package/src/composables/useSelectedResult.ts +0 -49
@@ -0,0 +1,289 @@
1
+ // Encore cadence math. Pure functions — no fs, no clock injection
2
+ // here (the caller passes `now` explicitly). Per the DSL spec each
3
+ // cadence shape determines:
4
+ //
5
+ // - which slot "now" falls into (currentCycleSlot)
6
+ // - the cycle's start date (cycleStart)
7
+ // - the cycle's deadline date (cycleDeadline)
8
+ // - the on-disk cycle id string (formatCycleId), used to name the
9
+ // per-cycle file under obligations/<id>/<cycleId>.md
10
+ //
11
+ // The five v1 cadences are: annual, biannual, monthly, weekly, daily.
12
+ // Quarterly / one-shot / custom-cron are out of scope. Days are
13
+ // capped at 28 in validators to avoid February edge cases; the math
14
+ // here trusts validated input. ISO dates everywhere (YYYY-MM-DD).
15
+
16
+ import { z } from "zod";
17
+
18
+ // ── ISO date helpers ────────────────────────────────────────────
19
+
20
+ /** Format a Date as `YYYY-MM-DD` in the server's LOCAL timezone.
21
+ * Encore obligations are wall-clock obligations ("pay rent on the
22
+ * 1st" means the 1st in the user's calendar, not the 1st in UTC),
23
+ * so cycle boundaries must follow the host's wall clock. Matches
24
+ * the journal subsystem's `toLocalIsoDate` convention in
25
+ * `server/utils/date.ts`. */
26
+ export function isoDate(date: Date): string {
27
+ const year = date.getFullYear();
28
+ const month = String(date.getMonth() + 1).padStart(2, "0");
29
+ const day = String(date.getDate()).padStart(2, "0");
30
+ return `${year}-${month}-${day}`;
31
+ }
32
+
33
+ function localDate(year: number, monthZero: number, day: number): Date {
34
+ return new Date(year, monthZero, day);
35
+ }
36
+
37
+ /** Parse a YYYY-MM-DD string to a local-midnight Date. */
38
+ function parseLocalIsoDate(iso: string): Date {
39
+ const [year, month, day] = iso.split("-").map(Number);
40
+ return new Date(year, month - 1, day);
41
+ }
42
+
43
+ /** Add N calendar days to an ISO date (returns ISO). */
44
+ export function addDays(iso: string, days: number): string {
45
+ const [year, month, day] = iso.split("-").map(Number);
46
+ return isoDate(localDate(year, month - 1, day + days));
47
+ }
48
+
49
+ /** Lexicographic ISO compare. -1 / 0 / 1. */
50
+ export function compareIsoDates(lhs: string, rhs: string): number {
51
+ if (lhs < rhs) return -1;
52
+ if (lhs > rhs) return 1;
53
+ return 0;
54
+ }
55
+
56
+ // ── ISO week helpers (used by weekly cadence) ───────────────────
57
+
58
+ const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
59
+
60
+ /** ISO 8601 week number for a date. The ISO week year matches the
61
+ * Thursday of the week — handy for the early-Jan / late-Dec edge
62
+ * cases where a date's calendar year and ISO-week year differ. */
63
+ function isoWeekYearAndNumber(date: Date): { year: number; week: number } {
64
+ // Copy to avoid mutating caller.
65
+ const thursdayOfWeek = new Date(date.getFullYear(), date.getMonth(), date.getDate());
66
+ const dayNum = (thursdayOfWeek.getDay() + 6) % 7; // Mon=0 … Sun=6
67
+ thursdayOfWeek.setDate(thursdayOfWeek.getDate() - dayNum + 3);
68
+ const firstThursday = new Date(thursdayOfWeek.getFullYear(), 0, 4);
69
+ const firstThursdayDayNum = (firstThursday.getDay() + 6) % 7;
70
+ const firstThursdayWeekStart = new Date(firstThursday);
71
+ firstThursdayWeekStart.setDate(firstThursday.getDate() - firstThursdayDayNum);
72
+ const diffMs = thursdayOfWeek.getTime() - firstThursdayWeekStart.getTime();
73
+ const week = 1 + Math.round(diffMs / MS_PER_WEEK);
74
+ return { year: thursdayOfWeek.getFullYear(), week };
75
+ }
76
+
77
+ /** Monday of the given ISO week (returns ISO date). */
78
+ function isoWeekMonday(year: number, week: number): string {
79
+ // Jan 4 is always in week 1.
80
+ const jan4 = new Date(year, 0, 4);
81
+ const jan4DayNum = (jan4.getDay() + 6) % 7;
82
+ const mondayOfWeek1 = new Date(jan4);
83
+ mondayOfWeek1.setDate(jan4.getDate() - jan4DayNum);
84
+ const target = new Date(mondayOfWeek1);
85
+ target.setDate(mondayOfWeek1.getDate() + (week - 1) * 7);
86
+ return isoDate(target);
87
+ }
88
+
89
+ // ── Cadence Zod schema ──────────────────────────────────────────
90
+
91
+ const dayInMonth = z.number().int().min(1).max(28);
92
+ const monthOfYear = z.number().int().min(1).max(12);
93
+
94
+ const cycleEntry = z.object({ month: monthOfYear, day: dayInMonth });
95
+
96
+ const annual = z.object({
97
+ type: z.literal("annual"),
98
+ cycles: z.tuple([cycleEntry]),
99
+ });
100
+
101
+ const biannual = z.object({
102
+ type: z.literal("biannual"),
103
+ cycles: z.tuple([cycleEntry, cycleEntry]),
104
+ });
105
+
106
+ const monthly = z.object({
107
+ type: z.literal("monthly"),
108
+ day: dayInMonth,
109
+ });
110
+
111
+ const DAY_OF_WEEK = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
112
+ export type DayOfWeek = (typeof DAY_OF_WEEK)[number];
113
+
114
+ const weekly = z.object({
115
+ type: z.literal("weekly"),
116
+ dayOfWeek: z.enum(DAY_OF_WEEK),
117
+ });
118
+
119
+ const daily = z.object({
120
+ type: z.literal("daily"),
121
+ });
122
+
123
+ export const CadenceSchema = z.discriminatedUnion("type", [annual, biannual, monthly, weekly, daily]).superRefine((cadence, ctx) => {
124
+ if (cadence.type === "biannual") {
125
+ const [first, second] = cadence.cycles;
126
+ if (first.month > second.month || (first.month === second.month && first.day >= second.day)) {
127
+ ctx.addIssue({
128
+ code: z.ZodIssueCode.custom,
129
+ message: "biannual cycles must be in calendar order (first cycle before second)",
130
+ path: ["cycles"],
131
+ });
132
+ }
133
+ }
134
+ });
135
+
136
+ export type Cadence = z.infer<typeof CadenceSchema>;
137
+
138
+ // ── Slot identification ─────────────────────────────────────────
139
+
140
+ export type CycleSlot =
141
+ | { kind: "annual"; year: number }
142
+ | { kind: "biannual"; year: number; half: 1 | 2 }
143
+ | { kind: "monthly"; year: number; month: number }
144
+ | { kind: "weekly"; year: number; week: number }
145
+ | { kind: "daily"; iso: string };
146
+
147
+ /** Find the cycle slot a given date falls into for this cadence.
148
+ * "Falls into" = the slot whose [start, deadline] range contains
149
+ * `now`; for annual / biannual we pick the slot whose deadline is
150
+ * the next upcoming one (so the cycle is "active" until its
151
+ * deadline passes; the day AFTER the deadline rolls into the next
152
+ * cycle, matching the spec's cycle-start = day-after-prev-deadline
153
+ * rule). */
154
+ export function currentCycleSlot(cadence: Cadence, now: Date): CycleSlot {
155
+ // Compare as ISO date strings — the deadline is date-only, and
156
+ // comparing `now` (a full timestamp) against a midnight Date
157
+ // would roll over one day early on the deadline day. ISO-string
158
+ // lexical compare is calendar-correct for YYYY-MM-DD.
159
+ const todayIso = isoDate(now);
160
+ const year = now.getFullYear();
161
+ if (cadence.type === "annual") {
162
+ const [{ month, day }] = cadence.cycles;
163
+ const thisYearDeadlineIso = isoDate(localDate(year, month - 1, day));
164
+ return { kind: "annual", year: todayIso <= thisYearDeadlineIso ? year : year + 1 };
165
+ }
166
+ if (cadence.type === "biannual") {
167
+ const [first, second] = cadence.cycles;
168
+ const firstDeadlineIso = isoDate(localDate(year, first.month - 1, first.day));
169
+ const secondDeadlineIso = isoDate(localDate(year, second.month - 1, second.day));
170
+ if (todayIso <= firstDeadlineIso) return { kind: "biannual", year, half: 1 };
171
+ if (todayIso <= secondDeadlineIso) return { kind: "biannual", year, half: 2 };
172
+ return { kind: "biannual", year: year + 1, half: 1 };
173
+ }
174
+ if (cadence.type === "monthly") {
175
+ const monthZero = now.getMonth();
176
+ const deadlineThisMonthIso = isoDate(localDate(year, monthZero, cadence.day));
177
+ if (todayIso <= deadlineThisMonthIso) {
178
+ return { kind: "monthly", year, month: monthZero + 1 };
179
+ }
180
+ const next = localDate(year, monthZero + 1, 1);
181
+ return { kind: "monthly", year: next.getFullYear(), month: next.getMonth() + 1 };
182
+ }
183
+ if (cadence.type === "weekly") {
184
+ const targetIdx = DAY_OF_WEEK.indexOf(cadence.dayOfWeek);
185
+ const { year: isoYear, week } = isoWeekYearAndNumber(now);
186
+ const monday = isoWeekMonday(isoYear, week);
187
+ const deadlineThisWeek = addDays(monday, targetIdx);
188
+ if (isoDate(now) <= deadlineThisWeek) {
189
+ return { kind: "weekly", year: isoYear, week };
190
+ }
191
+ const nextMondayIso = addDays(monday, 7);
192
+ const next = isoWeekYearAndNumber(parseLocalIsoDate(nextMondayIso));
193
+ return { kind: "weekly", year: next.year, week: next.week };
194
+ }
195
+ // daily
196
+ return { kind: "daily", iso: isoDate(now) };
197
+ }
198
+
199
+ /** ISO date of the cycle's deadline. */
200
+ export function cycleDeadline(cadence: Cadence, slot: CycleSlot): string {
201
+ if (cadence.type === "annual" && slot.kind === "annual") {
202
+ const [{ month, day }] = cadence.cycles;
203
+ return isoDate(localDate(slot.year, month - 1, day));
204
+ }
205
+ if (cadence.type === "biannual" && slot.kind === "biannual") {
206
+ const entry = cadence.cycles[slot.half - 1];
207
+ return isoDate(localDate(slot.year, entry.month - 1, entry.day));
208
+ }
209
+ if (cadence.type === "monthly" && slot.kind === "monthly") {
210
+ return isoDate(localDate(slot.year, slot.month - 1, cadence.day));
211
+ }
212
+ if (cadence.type === "weekly" && slot.kind === "weekly") {
213
+ const monday = isoWeekMonday(slot.year, slot.week);
214
+ const targetIdx = DAY_OF_WEEK.indexOf(cadence.dayOfWeek);
215
+ return addDays(monday, targetIdx);
216
+ }
217
+ if (cadence.type === "daily" && slot.kind === "daily") {
218
+ return slot.iso;
219
+ }
220
+ throw new Error(`cadence/slot mismatch: cadence.type=${cadence.type} slot.kind=${slot.kind}`);
221
+ }
222
+
223
+ /** ISO date of the cycle's start. For annual/biannual that's the
224
+ * day after the previous slot's deadline; for monthly the 1st of
225
+ * the month; weekly the Monday of the ISO week; daily same as
226
+ * deadline. */
227
+ export function cycleStart(cadence: Cadence, slot: CycleSlot): string {
228
+ if (cadence.type === "annual" && slot.kind === "annual") {
229
+ const [{ month, day }] = cadence.cycles;
230
+ const prevDeadline = isoDate(localDate(slot.year - 1, month - 1, day));
231
+ return addDays(prevDeadline, 1);
232
+ }
233
+ if (cadence.type === "biannual" && slot.kind === "biannual") {
234
+ if (slot.half === 1) {
235
+ const [, second] = cadence.cycles;
236
+ const prevDeadline = isoDate(localDate(slot.year - 1, second.month - 1, second.day));
237
+ return addDays(prevDeadline, 1);
238
+ }
239
+ const [first] = cadence.cycles;
240
+ const prevDeadline = isoDate(localDate(slot.year, first.month - 1, first.day));
241
+ return addDays(prevDeadline, 1);
242
+ }
243
+ if (cadence.type === "monthly" && slot.kind === "monthly") {
244
+ return isoDate(localDate(slot.year, slot.month - 1, 1));
245
+ }
246
+ if (cadence.type === "weekly" && slot.kind === "weekly") {
247
+ return isoWeekMonday(slot.year, slot.week);
248
+ }
249
+ if (cadence.type === "daily" && slot.kind === "daily") {
250
+ return slot.iso;
251
+ }
252
+ throw new Error(`cadence/slot mismatch: cadence.type=${cadence.type} slot.kind=${slot.kind}`);
253
+ }
254
+
255
+ /** On-disk cycle id string. Stable / deterministic — used as the
256
+ * per-cycle markdown file name under obligations/<id>/<cycleId>.md. */
257
+ export function formatCycleId(slot: CycleSlot): string {
258
+ if (slot.kind === "annual") return `${slot.year}`;
259
+ if (slot.kind === "biannual") return `${slot.year}-h${slot.half}`;
260
+ if (slot.kind === "monthly") return `${slot.year}-${String(slot.month).padStart(2, "0")}`;
261
+ if (slot.kind === "weekly") return `${slot.year}-W${String(slot.week).padStart(2, "0")}`;
262
+ return slot.iso;
263
+ }
264
+
265
+ /** Advance to the next slot after `current` for this cadence. Used
266
+ * by next-cycle provisioning when a cycle closes. */
267
+ export function nextSlot(cadence: Cadence, current: CycleSlot): CycleSlot {
268
+ if (cadence.type === "annual" && current.kind === "annual") {
269
+ return { kind: "annual", year: current.year + 1 };
270
+ }
271
+ if (cadence.type === "biannual" && current.kind === "biannual") {
272
+ if (current.half === 1) return { kind: "biannual", year: current.year, half: 2 };
273
+ return { kind: "biannual", year: current.year + 1, half: 1 };
274
+ }
275
+ if (cadence.type === "monthly" && current.kind === "monthly") {
276
+ const next = localDate(current.year, current.month, 1);
277
+ return { kind: "monthly", year: next.getFullYear(), month: next.getMonth() + 1 };
278
+ }
279
+ if (cadence.type === "weekly" && current.kind === "weekly") {
280
+ const monday = isoWeekMonday(current.year, current.week);
281
+ const nextMondayIso = addDays(monday, 7);
282
+ const { year, week } = isoWeekYearAndNumber(parseLocalIsoDate(nextMondayIso));
283
+ return { kind: "weekly", year, week };
284
+ }
285
+ if (cadence.type === "daily" && current.kind === "daily") {
286
+ return { kind: "daily", iso: addDays(current.iso, 1) };
287
+ }
288
+ throw new Error(`cadence/slot mismatch: cadence.type=${cadence.type} current.kind=${current.kind}`);
289
+ }
@@ -0,0 +1,288 @@
1
+ // Encore DSL Zod schema.
2
+ //
3
+ // One obligation = one DSL document, validated end-to-end on every
4
+ // setup / amendDefinition call. The discriminated union on `type`
5
+ // (payment | service) enforces `currency` required iff payment. The
6
+ // cross-field rules (cadence cycle-count, step-field ownership, at-
7
+ // expression validity, etc.) live in superRefine blocks at the leaf
8
+ // or document level.
9
+ //
10
+ // Naming convention (a deliberate distinction):
11
+ // - KEBAB regex (slug ids): obligation id, target id, step id —
12
+ // these become file/folder names and routes, so case-sensitive
13
+ // paths matter.
14
+ // - IDENTIFIER regex (field names): camelCase or kebab — Claude
15
+ // composes "invoiceReceivedOn" / "paidOn" naturally and the
16
+ // surrounding codebase uses camelCase identifiers everywhere.
17
+ //
18
+ // See plans/feat-encore-plugin.md §The Encore DSL for the full
19
+ // natural-language spec; this file is its executable form.
20
+
21
+ import { z } from "zod";
22
+ import { CadenceSchema } from "./cadence.js";
23
+ import { atExprSchema, parseAtExpression } from "./at-expression.js";
24
+ import { resolveAtExpression } from "./at-resolver.js";
25
+
26
+ const KEBAB = /^[a-z][a-z0-9-]*$/;
27
+ const IDENTIFIER = /^[a-z][a-zA-Z0-9_-]*$/;
28
+ const ISO_4217 = /^[A-Z]{3}$/;
29
+
30
+ const kebabId = z.string().regex(KEBAB, "must be kebab-case (lowercase letters, digits, hyphens; starts with a letter)");
31
+ const fieldName = z.string().regex(IDENTIFIER, "must be a valid identifier (lowercase start; letters / digits / _ / -)");
32
+
33
+ // ── formSchema field ────────────────────────────────────────────
34
+
35
+ const FIELD_TYPES = ["string", "text", "url", "email", "date", "number", "boolean", "enum"] as const;
36
+ export type FormFieldType = (typeof FIELD_TYPES)[number];
37
+
38
+ const FormField = z
39
+ .object({
40
+ name: fieldName,
41
+ type: z.enum(FIELD_TYPES),
42
+ label: z.string().min(1),
43
+ required: z.boolean().optional(),
44
+ placeholder: z.string().optional(),
45
+ options: z.array(z.string().min(1)).optional(),
46
+ })
47
+ .superRefine((field, ctx) => {
48
+ if (field.type === "enum") {
49
+ if (!field.options || field.options.length === 0) {
50
+ ctx.addIssue({
51
+ code: z.ZodIssueCode.custom,
52
+ message: "type=enum requires non-empty options[]",
53
+ path: ["options"],
54
+ });
55
+ }
56
+ } else if (field.options && field.options.length > 0) {
57
+ ctx.addIssue({
58
+ code: z.ZodIssueCode.custom,
59
+ message: `options[] is only meaningful for type=enum; got type=${field.type}`,
60
+ path: ["options"],
61
+ });
62
+ }
63
+ });
64
+
65
+ export type FormFieldDef = z.infer<typeof FormField>;
66
+
67
+ const FormSchema = z.object({
68
+ fields: z.array(FormField).min(1),
69
+ });
70
+
71
+ // ── target / step ───────────────────────────────────────────────
72
+
73
+ const Target = z.object({
74
+ id: kebabId,
75
+ displayName: z.string().min(1),
76
+ /** Pre-fill map — field-name → default value. The LLM treats
77
+ * presence of a default as "don't ask; auto-record". Validated
78
+ * against formSchema in the doc-level superRefine. */
79
+ defaults: z.record(z.string(), z.unknown()).optional(),
80
+ });
81
+
82
+ export type TargetDef = z.infer<typeof Target>;
83
+
84
+ const Severity = z.enum(["info", "warning", "urgent"]);
85
+ export type Severity = z.infer<typeof Severity>;
86
+
87
+ const FiringPhase = z.object({
88
+ at: atExprSchema({ allowStepDeadline: true }),
89
+ severity: Severity,
90
+ });
91
+
92
+ const Step = z.object({
93
+ id: kebabId,
94
+ displayName: z.string().min(1),
95
+ /** When this step's deadline falls. Resolved against
96
+ * cycleStart / cycleDeadline — step-deadline anchor not allowed
97
+ * here (it would be self-referential). */
98
+ deadline: atExprSchema({ allowStepDeadline: false }),
99
+ firingPlan: z.array(FiringPhase).min(1),
100
+ /** Field names this step is responsible for. Must reference
101
+ * existing formSchema field names; checked at document level. */
102
+ fields: z.array(fieldName),
103
+ });
104
+
105
+ export type StepDef = z.infer<typeof Step>;
106
+
107
+ // ── carryForward ────────────────────────────────────────────────
108
+
109
+ const CarryForward = z.object({
110
+ body: z.enum(["empty", "copy"]).default("empty"),
111
+ });
112
+
113
+ // ── top-level DSL (discriminated union on type) ─────────────────
114
+
115
+ const STATUS = ["active", "paused", "retired"] as const;
116
+
117
+ const sharedFields = {
118
+ version: z.literal(1),
119
+ /** Generated server-side from displayName at setup; the DSL Claude
120
+ * composes omits it. We keep the field optional in the input
121
+ * schema so amend operations work; setup explicitly strips. */
122
+ id: kebabId.optional(),
123
+ displayName: z.string().min(1),
124
+ status: z.enum(STATUS).default("active"),
125
+ /** Generated server-side at setup. */
126
+ createdAt: z.string().optional(),
127
+
128
+ cadence: CadenceSchema,
129
+ targets: z.array(Target).min(1),
130
+ steps: z.array(Step).min(1),
131
+ formSchema: FormSchema,
132
+ carryForward: CarryForward.optional(),
133
+ };
134
+
135
+ const PaymentDsl = z.object({
136
+ ...sharedFields,
137
+ type: z.literal("payment"),
138
+ currency: z.string().regex(ISO_4217, "currency must be a 3-letter uppercase ISO 4217 code (e.g. JPY, USD, EUR)"),
139
+ });
140
+
141
+ const ServiceDsl = z.object({
142
+ ...sharedFields,
143
+ type: z.literal("service"),
144
+ });
145
+
146
+ // ── cross-field validators (split out so the per-document
147
+ // superRefine stays under the cognitive-complexity threshold) ─────
148
+
149
+ interface Doc {
150
+ targets: TargetDef[];
151
+ steps: StepDef[];
152
+ formSchema: { fields: FormFieldDef[] };
153
+ }
154
+ type Ctx = z.RefinementCtx;
155
+
156
+ function validateUniqueIds(doc: Doc, ctx: Ctx): { fieldNames: Set<string> } {
157
+ const targetIds = new Set<string>();
158
+ for (const target of doc.targets) {
159
+ if (targetIds.has(target.id)) {
160
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate target id ${JSON.stringify(target.id)}`, path: ["targets"] });
161
+ }
162
+ targetIds.add(target.id);
163
+ }
164
+ const stepIds = new Set<string>();
165
+ for (const step of doc.steps) {
166
+ if (stepIds.has(step.id)) {
167
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate step id ${JSON.stringify(step.id)}`, path: ["steps"] });
168
+ }
169
+ stepIds.add(step.id);
170
+ }
171
+ const fieldNames = new Set<string>();
172
+ for (const field of doc.formSchema.fields) {
173
+ if (fieldNames.has(field.name)) {
174
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate formSchema field name ${JSON.stringify(field.name)}`, path: ["formSchema", "fields"] });
175
+ }
176
+ fieldNames.add(field.name);
177
+ }
178
+ return { fieldNames };
179
+ }
180
+
181
+ function validateFieldOwnership(doc: Doc, fieldNames: Set<string>, ctx: Ctx): void {
182
+ const claims = new Map<string, string[]>();
183
+ for (const step of doc.steps) {
184
+ for (const fname of step.fields) {
185
+ if (!fieldNames.has(fname)) {
186
+ ctx.addIssue({
187
+ code: z.ZodIssueCode.custom,
188
+ message: `step ${JSON.stringify(step.id)} references unknown formSchema field ${JSON.stringify(fname)}`,
189
+ path: ["steps"],
190
+ });
191
+ continue;
192
+ }
193
+ const list = claims.get(fname) ?? [];
194
+ list.push(step.id);
195
+ claims.set(fname, list);
196
+ }
197
+ }
198
+ for (const fname of fieldNames) {
199
+ const list = claims.get(fname) ?? [];
200
+ if (list.length === 0) {
201
+ // The LLM gets stuck here in a known loop: error #1 says "field not
202
+ // claimed", LLM removes the field, then `formSchema.fields.min(1)`
203
+ // fires and the LLM has no path forward (formSchema is non-optional
204
+ // and must have ≥1 field). Tell it the resolution explicitly: claim
205
+ // the field from a step. Reference the no-data pattern in the docs
206
+ // so the LLM can also pick the inverse (placeholder field claimed
207
+ // by the single step) when that fits the obligation better.
208
+ ctx.addIssue({
209
+ code: z.ZodIssueCode.custom,
210
+ message:
211
+ `formSchema field ${JSON.stringify(fname)} is not claimed by any step.fields[]. ` +
212
+ `Add ${JSON.stringify(fname)} to exactly one of the entries in steps[].fields[] ` +
213
+ `(every formSchema field must be claimed by one step). ` +
214
+ `If the obligation has nothing to record, see the "obligation with nothing to record" example in config/helps/encore-dsl.md ` +
215
+ `— you still need one placeholder field claimed by the step.`,
216
+ path: ["formSchema", "fields"],
217
+ });
218
+ } else if (list.length > 1) {
219
+ const owners = list.map((stepId) => JSON.stringify(stepId)).join(", ");
220
+ ctx.addIssue({
221
+ code: z.ZodIssueCode.custom,
222
+ message: `formSchema field ${JSON.stringify(fname)} is claimed by multiple steps: ${owners}`,
223
+ path: ["formSchema", "fields"],
224
+ });
225
+ }
226
+ }
227
+ }
228
+
229
+ function validateDefaultKeys(doc: Doc, fieldNames: Set<string>, ctx: Ctx): void {
230
+ for (const target of doc.targets) {
231
+ if (!target.defaults) continue;
232
+ for (const key of Object.keys(target.defaults)) {
233
+ if (!fieldNames.has(key)) {
234
+ ctx.addIssue({
235
+ code: z.ZodIssueCode.custom,
236
+ message: `target ${JSON.stringify(target.id)} default for ${JSON.stringify(key)} references unknown formSchema field`,
237
+ path: ["targets"],
238
+ });
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ function validateFiringPlanOrder(doc: Doc, ctx: Ctx): void {
245
+ // Use a representative cycle (the actual date doesn't matter, only
246
+ // the relative ordering of phases against it).
247
+ const anchorsBase = { cycleStart: "2026-01-01", cycleDeadline: "2026-12-31" };
248
+ for (const step of doc.steps) {
249
+ let stepDeadlineIso: string | undefined;
250
+ try {
251
+ const expr = parseAtExpression(step.deadline, { allowStepDeadline: false });
252
+ stepDeadlineIso = resolveAtExpression(expr, anchorsBase);
253
+ } catch {
254
+ continue;
255
+ }
256
+ const anchors = { ...anchorsBase, stepDeadline: stepDeadlineIso };
257
+ let prev: string | null = null;
258
+ for (let i = 0; i < step.firingPlan.length; i++) {
259
+ const phase = step.firingPlan[i];
260
+ let resolved: string;
261
+ try {
262
+ const expr = parseAtExpression(phase.at, { allowStepDeadline: true });
263
+ resolved = resolveAtExpression(expr, anchors);
264
+ } catch {
265
+ continue;
266
+ }
267
+ if (prev !== null && resolved < prev) {
268
+ ctx.addIssue({
269
+ code: z.ZodIssueCode.custom,
270
+ message: `step ${JSON.stringify(step.id)}: firingPlan[${i}].at ${JSON.stringify(phase.at)} resolves before the previous phase — phases must be chronologically ordered`,
271
+ path: ["steps"],
272
+ });
273
+ }
274
+ prev = resolved;
275
+ }
276
+ }
277
+ }
278
+
279
+ /** Top-level DSL union. Input shape — the post-superRefine resolved
280
+ * shape lives in `EncoreDsl` below. */
281
+ export const EncoreDslInput = z.discriminatedUnion("type", [PaymentDsl, ServiceDsl]).superRefine((doc, ctx) => {
282
+ const { fieldNames } = validateUniqueIds(doc, ctx);
283
+ validateFieldOwnership(doc, fieldNames, ctx);
284
+ validateDefaultKeys(doc, fieldNames, ctx);
285
+ validateFiringPlanOrder(doc, ctx);
286
+ });
287
+
288
+ export type EncoreDsl = z.infer<typeof EncoreDslInput>;
@@ -69,7 +69,7 @@ export type NotificationPriority = (typeof NOTIFICATION_PRIORITIES)[keyof typeof
69
69
  // router / page component needs to jump to a specific item. Omitting
70
70
  // the identifier lands on the feature's index view.
71
71
  export type NotificationTarget =
72
- | { view: typeof NOTIFICATION_VIEWS.chat; sessionId: string; resultUuid?: string }
72
+ | { view: typeof NOTIFICATION_VIEWS.chat; sessionId: string }
73
73
  | { view: typeof NOTIFICATION_VIEWS.todos; itemId?: string }
74
74
  | { view: typeof NOTIFICATION_VIEWS.calendar }
75
75
  | { view: typeof NOTIFICATION_VIEWS.automations; taskId?: string }
@@ -96,6 +96,7 @@ export type NotificationAction =
96
96
  * client is on. Other notification kinds may opt in later. */
97
97
  export interface NotificationI18n {
98
98
  titleKey: string;
99
+ titleParams?: Readonly<Record<string, string | number | readonly string[]>>;
99
100
  bodyKey?: string;
100
101
  bodyParams?: Readonly<Record<string, string | number | readonly string[]>>;
101
102
  }
@@ -63,7 +63,13 @@ export interface SessionSummary {
63
63
  isBookmarked?: boolean;
64
64
  // Live state from the server session store (present when the
65
65
  // session has an active in-memory entry on the server).
66
+ //
67
+ // `isRunning` — broad: agent turn live OR background generation
68
+ // pending. Drives the sidebar busy indicator.
69
+ // `liveIsRunning` — narrow: mirrors the DELETE 409 gate exactly
70
+ // (#1195). `false` ⇒ a DELETE on this session will be accepted.
66
71
  isRunning?: boolean;
72
+ liveIsRunning?: boolean;
67
73
  hasUnread?: boolean;
68
74
  statusMessage?: string;
69
75
  }
package/src/types/sse.ts CHANGED
@@ -17,6 +17,11 @@ export interface SseToolCallResult {
17
17
  type: typeof EVENT_TYPES.toolCallResult;
18
18
  toolUseId: string;
19
19
  content: string;
20
+ /** Set when the tool-result block carried `is_error: true` —
21
+ * forwarded from `AgentEvent.toolCallResult.isError` so the
22
+ * frontend can render the chip distinctly. Drives the MCP
23
+ * failure monitor (#1353). */
24
+ isError?: boolean;
20
25
  }
21
26
 
22
27
  export interface SseStatus {
@@ -3,6 +3,8 @@
3
3
  // session domain types, the pending-calls helper, etc.) can refer
4
4
  // to it without depending on a Vue file.
5
5
 
6
+ import type { McpHint } from "../utils/agent/mcpHint";
7
+
6
8
  export interface ToolCallHistoryItem {
7
9
  toolUseId: string;
8
10
  toolName: string;
@@ -10,4 +12,9 @@ export interface ToolCallHistoryItem {
10
12
  timestamp: number;
11
13
  result?: string;
12
14
  error?: string;
15
+ /** Structured hint surfaced next to `error` when the failing tool
16
+ * belongs to a catalogued MCP server. Lets the right-sidebar
17
+ * render setup-guide links / required-key reminders without the
18
+ * caller re-parsing the tool name. (#1354) */
19
+ mcpHint?: McpHint;
13
20
  }