@vibe-hero/server 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 (150) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +151 -0
  3. package/dist/catalog/bundled/claude-code/.gitkeep +0 -0
  4. package/dist/catalog/bundled/claude-code/context-management.yaml +302 -0
  5. package/dist/catalog/bundled/claude-code/planning.yaml +313 -0
  6. package/dist/catalog/bundled/claude-code/subagents.yaml +357 -0
  7. package/dist/catalog/bundled/general/.gitkeep +0 -0
  8. package/dist/catalog/bundled/general/_placeholder.yaml +39 -0
  9. package/dist/catalog/bundled/general/task-decomposition.yaml +390 -0
  10. package/dist/catalog/bundled/index.d.ts +39 -0
  11. package/dist/catalog/bundled/index.d.ts.map +1 -0
  12. package/dist/catalog/bundled/index.js +41 -0
  13. package/dist/catalog/bundled/index.js.map +1 -0
  14. package/dist/catalog/fetcher.d.ts +201 -0
  15. package/dist/catalog/fetcher.d.ts.map +1 -0
  16. package/dist/catalog/fetcher.js +452 -0
  17. package/dist/catalog/fetcher.js.map +1 -0
  18. package/dist/catalog/loader.d.ts +165 -0
  19. package/dist/catalog/loader.d.ts.map +1 -0
  20. package/dist/catalog/loader.js +241 -0
  21. package/dist/catalog/loader.js.map +1 -0
  22. package/dist/catalog/resolve.d.ts +85 -0
  23. package/dist/catalog/resolve.d.ts.map +1 -0
  24. package/dist/catalog/resolve.js +103 -0
  25. package/dist/catalog/resolve.js.map +1 -0
  26. package/dist/cli/getOffer.d.ts +38 -0
  27. package/dist/cli/getOffer.d.ts.map +1 -0
  28. package/dist/cli/getOffer.js +150 -0
  29. package/dist/cli/getOffer.js.map +1 -0
  30. package/dist/cli/index.d.ts +46 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +88 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/config.d.ts +34 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +63 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/engine/elo.d.ts +76 -0
  39. package/dist/engine/elo.d.ts.map +1 -0
  40. package/dist/engine/elo.js +79 -0
  41. package/dist/engine/elo.js.map +1 -0
  42. package/dist/engine/graduation.d.ts +108 -0
  43. package/dist/engine/graduation.d.ts.map +1 -0
  44. package/dist/engine/graduation.js +161 -0
  45. package/dist/engine/graduation.js.map +1 -0
  46. package/dist/engine/lapse.d.ts +80 -0
  47. package/dist/engine/lapse.d.ts.map +1 -0
  48. package/dist/engine/lapse.js +125 -0
  49. package/dist/engine/lapse.js.map +1 -0
  50. package/dist/engine/selection.d.ts +84 -0
  51. package/dist/engine/selection.d.ts.map +1 -0
  52. package/dist/engine/selection.js +119 -0
  53. package/dist/engine/selection.js.map +1 -0
  54. package/dist/grading/deterministic.d.ts +102 -0
  55. package/dist/grading/deterministic.d.ts.map +1 -0
  56. package/dist/grading/deterministic.js +118 -0
  57. package/dist/grading/deterministic.js.map +1 -0
  58. package/dist/grading/freeform.d.ts +64 -0
  59. package/dist/grading/freeform.d.ts.map +1 -0
  60. package/dist/grading/freeform.js +85 -0
  61. package/dist/grading/freeform.js.map +1 -0
  62. package/dist/index.d.ts +52 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +91 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/observation/hookEvents.d.ts +113 -0
  67. package/dist/observation/hookEvents.d.ts.map +1 -0
  68. package/dist/observation/hookEvents.js +170 -0
  69. package/dist/observation/hookEvents.js.map +1 -0
  70. package/dist/observation/offers.d.ts +215 -0
  71. package/dist/observation/offers.d.ts.map +1 -0
  72. package/dist/observation/offers.js +327 -0
  73. package/dist/observation/offers.js.map +1 -0
  74. package/dist/observation/source.d.ts +133 -0
  75. package/dist/observation/source.d.ts.map +1 -0
  76. package/dist/observation/source.js +105 -0
  77. package/dist/observation/source.js.map +1 -0
  78. package/dist/profile/migrate.d.ts +122 -0
  79. package/dist/profile/migrate.d.ts.map +1 -0
  80. package/dist/profile/migrate.js +147 -0
  81. package/dist/profile/migrate.js.map +1 -0
  82. package/dist/profile/store.d.ts +84 -0
  83. package/dist/profile/store.d.ts.map +1 -0
  84. package/dist/profile/store.js +267 -0
  85. package/dist/profile/store.js.map +1 -0
  86. package/dist/schemas/common.d.ts +95 -0
  87. package/dist/schemas/common.d.ts.map +1 -0
  88. package/dist/schemas/common.js +106 -0
  89. package/dist/schemas/common.js.map +1 -0
  90. package/dist/schemas/content.d.ts +828 -0
  91. package/dist/schemas/content.d.ts.map +1 -0
  92. package/dist/schemas/content.js +219 -0
  93. package/dist/schemas/content.js.map +1 -0
  94. package/dist/schemas/profile.d.ts +599 -0
  95. package/dist/schemas/profile.d.ts.map +1 -0
  96. package/dist/schemas/profile.js +177 -0
  97. package/dist/schemas/profile.js.map +1 -0
  98. package/dist/schemas/tools.d.ts +1581 -0
  99. package/dist/schemas/tools.d.ts.map +1 -0
  100. package/dist/schemas/tools.js +286 -0
  101. package/dist/schemas/tools.js.map +1 -0
  102. package/dist/tools/config.d.ts +51 -0
  103. package/dist/tools/config.d.ts.map +1 -0
  104. package/dist/tools/config.js +104 -0
  105. package/dist/tools/config.js.map +1 -0
  106. package/dist/tools/gate.d.ts +50 -0
  107. package/dist/tools/gate.d.ts.map +1 -0
  108. package/dist/tools/gate.js +67 -0
  109. package/dist/tools/gate.js.map +1 -0
  110. package/dist/tools/guidance.d.ts +36 -0
  111. package/dist/tools/guidance.d.ts.map +1 -0
  112. package/dist/tools/guidance.js +117 -0
  113. package/dist/tools/guidance.js.map +1 -0
  114. package/dist/tools/listTopics.d.ts +55 -0
  115. package/dist/tools/listTopics.d.ts.map +1 -0
  116. package/dist/tools/listTopics.js +78 -0
  117. package/dist/tools/listTopics.js.map +1 -0
  118. package/dist/tools/offers.d.ts +60 -0
  119. package/dist/tools/offers.d.ts.map +1 -0
  120. package/dist/tools/offers.js +152 -0
  121. package/dist/tools/offers.js.map +1 -0
  122. package/dist/tools/placeholders.d.ts +27 -0
  123. package/dist/tools/placeholders.d.ts.map +1 -0
  124. package/dist/tools/placeholders.js +49 -0
  125. package/dist/tools/placeholders.js.map +1 -0
  126. package/dist/tools/recordObservation.d.ts +52 -0
  127. package/dist/tools/recordObservation.d.ts.map +1 -0
  128. package/dist/tools/recordObservation.js +87 -0
  129. package/dist/tools/recordObservation.js.map +1 -0
  130. package/dist/tools/startQuiz.d.ts +82 -0
  131. package/dist/tools/startQuiz.d.ts.map +1 -0
  132. package/dist/tools/startQuiz.js +180 -0
  133. package/dist/tools/startQuiz.js.map +1 -0
  134. package/dist/tools/status.d.ts +59 -0
  135. package/dist/tools/status.d.ts.map +1 -0
  136. package/dist/tools/status.js +133 -0
  137. package/dist/tools/status.js.map +1 -0
  138. package/dist/tools/submitAnswer.d.ts +156 -0
  139. package/dist/tools/submitAnswer.d.ts.map +1 -0
  140. package/dist/tools/submitAnswer.js +402 -0
  141. package/dist/tools/submitAnswer.js.map +1 -0
  142. package/dist/tools/types.d.ts +82 -0
  143. package/dist/tools/types.d.ts.map +1 -0
  144. package/dist/tools/types.js +48 -0
  145. package/dist/tools/types.js.map +1 -0
  146. package/dist/tools/us2/standing.d.ts +111 -0
  147. package/dist/tools/us2/standing.d.ts.map +1 -0
  148. package/dist/tools/us2/standing.js +143 -0
  149. package/dist/tools/us2/standing.js.map +1 -0
  150. package/package.json +62 -0
@@ -0,0 +1,327 @@
1
+ /**
2
+ * @file Offer engine (T035) — cadence + anti-fatigue decision logic.
3
+ *
4
+ * This is the brain behind the observation → offer pipeline. It does two
5
+ * trigger-only jobs (it NEVER scores — FR-005 / SC-003):
6
+ *
7
+ * 1. **Match** derived activity signals to candidate topic keys by scanning the
8
+ * loaded topics' {@link TriggerSignal}s ({@link matchCandidates}).
9
+ * 2. **Decide** whether an offer may surface at an end-of-work breakpoint, and
10
+ * for which key, honoring the configured cadence and the full anti-fatigue
11
+ * stack ({@link resolveOffer}) — within-session decline suppression
12
+ * (FR-020), configurable cadence off / per_session / per_topic (FR-020a),
13
+ * and cross-session decline backoff + global mute (FR-020b).
14
+ *
15
+ * Design: the decision core is **pure**. {@link resolveOffer},
16
+ * {@link matchCandidates}, {@link applyDecline}, and {@link applyAccept} take
17
+ * plain state + a `now` timestamp and return plain results — no IO, no clock, no
18
+ * randomness — so they are trivially testable and deterministic. The tool layer
19
+ * (`tools/offers.ts`, `tools/recordObservation.ts`) is the thin wrapper that
20
+ * reads the clock, loads the catalog, and persists via `updateProfile`.
21
+ *
22
+ * Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-005, FR-015..017,
23
+ * FR-019/020/020a/020b, SC-003), specs/001-vibe-hero-mvp/data-model.md
24
+ * (§ OfferLedger), specs/001-vibe-hero-mvp/contracts/mcp-tools.md
25
+ * (`record_observation` / `get_offer` / `record_offer_response`),
26
+ * src/config.ts (ASSESSMENT_CONFIG.declineMuteThreshold / backoff*).
27
+ */
28
+ import { ASSESSMENT_CONFIG } from "../config.js";
29
+ import { abilityKey } from "../schemas/common.js";
30
+ /**
31
+ * Does a single {@link TriggerSignal} match a single {@link ObservedSignal} for
32
+ * the given tool? A match requires the tool to agree and at least one of the
33
+ * signal's selectors to hit:
34
+ * - `match.toolName` — exact (case-sensitive) equality against `signal.toolName`.
35
+ * - `match.toolNamePattern` — regex tested against `signal.toolName`.
36
+ * - `match.mcpToolPattern` — regex tested against `signal.mcpTool`.
37
+ *
38
+ * A malformed regex pattern fails closed (no match) rather than throwing — a bad
39
+ * trigger declaration must never crash the observation intake path.
40
+ */
41
+ const triggerMatchesSignal = (trigger, signal) => {
42
+ const { match } = trigger;
43
+ if (match.toolName !== undefined &&
44
+ signal.toolName !== undefined &&
45
+ match.toolName === signal.toolName) {
46
+ return true;
47
+ }
48
+ if (match.toolNamePattern !== undefined && signal.toolName !== undefined) {
49
+ if (safeRegexTest(match.toolNamePattern, signal.toolName))
50
+ return true;
51
+ }
52
+ if (match.mcpToolPattern !== undefined && signal.mcpTool !== undefined) {
53
+ if (safeRegexTest(match.mcpToolPattern, signal.mcpTool))
54
+ return true;
55
+ }
56
+ return false;
57
+ };
58
+ /** Test `pattern` against `value`, failing closed on an invalid regex. */
59
+ const safeRegexTest = (pattern, value) => {
60
+ try {
61
+ return new RegExp(pattern).test(value);
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ };
67
+ /**
68
+ * Match derived signals against the loaded topics' trigger declarations and
69
+ * return the distinct candidate topics (deduped by {@link AbilityKey}, in
70
+ * topic-iteration order). Trigger-only (FR-015): this selects *which topic to
71
+ * offer* and never scores.
72
+ *
73
+ * Only triggers whose `tool` equals the observation's `tool` are considered, so
74
+ * a Claude Code Bash signal never trips a Codex topic. A topic with no matching
75
+ * trigger is omitted.
76
+ *
77
+ * @param topics - The loaded catalog topics (each carrying `triggerSignals`).
78
+ * @param tool - The host tool the activity belongs to.
79
+ * @param signals - The derived signals observed this turn.
80
+ * @returns The distinct offer candidates `{ key, title, reason }`.
81
+ */
82
+ export const matchCandidates = (topics, tool, signals) => {
83
+ const candidates = [];
84
+ const seen = new Set();
85
+ for (const topic of topics) {
86
+ const key = abilityKey(topic.class, topic.id);
87
+ if (seen.has(key))
88
+ continue;
89
+ // Find a trigger for THIS tool that some observed signal satisfies.
90
+ const hit = topic.triggerSignals.find((trigger) => trigger.tool === tool &&
91
+ signals.some((signal) => triggerMatchesSignal(trigger, signal)));
92
+ if (hit === undefined)
93
+ continue;
94
+ seen.add(key);
95
+ candidates.push({
96
+ key,
97
+ title: topic.title,
98
+ reason: describeMatch(hit),
99
+ });
100
+ }
101
+ return candidates;
102
+ };
103
+ /** A short, privacy-safe human reason for why a topic became a candidate. */
104
+ const describeMatch = (trigger) => {
105
+ const { match } = trigger;
106
+ if (match.toolName !== undefined) {
107
+ return `Observed ${match.toolName} activity, which exercises this topic.`;
108
+ }
109
+ if (match.toolNamePattern !== undefined) {
110
+ return `Observed tool activity matching this topic's trigger.`;
111
+ }
112
+ return `Observed MCP tool activity matching this topic's trigger.`;
113
+ };
114
+ /**
115
+ * Decide whether an end-of-work offer may surface, and for which key (FR-019
116
+ * non-interrupting timing is the caller's concern; this is the *whether/which*).
117
+ *
118
+ * Gates, in order (first failure wins):
119
+ * 1. `offerCadence === "off"` ⇒ `offers_off` (FR-020a).
120
+ * 2. `proactiveOffers === false` ⇒ `offers_off` (master switch, FR-031).
121
+ * 3. `mutedUntil` in the future ⇒ `cadence` (global mute, FR-020b).
122
+ * 4. `declinedThisSession` ⇒ `declined` (within-session suppression, FR-020).
123
+ * 5. `per_session` and an offer already surfaced this session ⇒ `cadence`
124
+ * (≤1 offer/session, FR-020a).
125
+ * 6. No candidate keys at all ⇒ `no_candidate`.
126
+ * 7. Otherwise pick the first candidate that is BOTH not already offered this
127
+ * session under `per_topic` (≤1 per distinct key/session, FR-020a) AND not
128
+ * within its cross-session `perTopicNextEligibleAt` backoff window
129
+ * (FR-020b). If none survives ⇒ `cadence`.
130
+ *
131
+ * Pure: no clock, no IO. `now` is supplied by the caller.
132
+ *
133
+ * @param state - The bundled offer state (config flags, ledger, backoff, candidates).
134
+ * @param now - The current instant, for muted/backoff comparisons.
135
+ * @returns An {@link OfferDecision}.
136
+ */
137
+ export const resolveOffer = (state, now) => {
138
+ const { proactiveOffers, offerCadence, ledger, backoff, candidates } = state;
139
+ // 1 + 2: offers disabled entirely.
140
+ if (offerCadence === "off" || !proactiveOffers) {
141
+ return { kind: "suppressed", reason: "offers_off" };
142
+ }
143
+ // 3: global cross-session mute (FR-020b) — N consecutive declines reached.
144
+ if (isMuted(backoff, now)) {
145
+ return { kind: "suppressed", reason: "cadence" };
146
+ }
147
+ // 4: a decline this session suppresses the rest of the session (FR-020).
148
+ if (ledger.declinedThisSession) {
149
+ return { kind: "suppressed", reason: "declined" };
150
+ }
151
+ // 5: per_session cap — at most one offer for the whole session (FR-020a).
152
+ if (offerCadence === "per_session" && ledger.offersThisSession >= 1) {
153
+ return { kind: "suppressed", reason: "cadence" };
154
+ }
155
+ // 6: nothing to offer.
156
+ if (candidates.length === 0) {
157
+ return { kind: "suppressed", reason: "no_candidate" };
158
+ }
159
+ // 7: pick the first candidate clearing the per-topic + backoff gates.
160
+ const alreadyOffered = new Set(ledger.offeredTopicKeys);
161
+ for (const key of candidates) {
162
+ if (offerCadence === "per_topic" && alreadyOffered.has(key)) {
163
+ // ≤1 offer per distinct key this session (FR-020a).
164
+ continue;
165
+ }
166
+ if (isBackedOff(backoff, key, now)) {
167
+ // Within the cross-session re-offer window for this topic (FR-020b).
168
+ continue;
169
+ }
170
+ return { kind: "offer", key };
171
+ }
172
+ // Every candidate was per-topic-exhausted or backed off.
173
+ return { kind: "suppressed", reason: "cadence" };
174
+ };
175
+ /** Is `mutedUntil` set and still in the future at `now`? (Global mute, FR-020b.) */
176
+ const isMuted = (backoff, now) => backoff.mutedUntil !== undefined &&
177
+ Date.parse(backoff.mutedUntil) > now.getTime();
178
+ /**
179
+ * Is `key` within its cross-session re-offer backoff window at `now`? A key with
180
+ * no recorded `perTopicNextEligibleAt` entry is always eligible (FR-020b).
181
+ */
182
+ const isBackedOff = (backoff, key, now) => {
183
+ const nextAt = backoff.perTopicNextEligibleAt[key];
184
+ return nextAt !== undefined && Date.parse(nextAt) > now.getTime();
185
+ };
186
+ /**
187
+ * The per-session ledger reset to a fresh `sessionId`. The cross-session backoff
188
+ * is intentionally NOT reset here — it persists across sessions until an accept
189
+ * resets it (FR-020b). Used by the tool layer when a new session begins.
190
+ */
191
+ export const freshLedger = (sessionId) => ({
192
+ sessionId,
193
+ offersThisSession: 0,
194
+ declinedThisSession: false,
195
+ offeredTopicKeys: [],
196
+ candidateKeys: [],
197
+ });
198
+ /**
199
+ * Reconcile the persisted ledger with the session being observed/queried. If the
200
+ * ledger's `sessionId` differs (or is the empty-string sentinel), the previous
201
+ * session's per-session accounting is stale, so return a {@link freshLedger}.
202
+ * Otherwise return the ledger unchanged. Pure.
203
+ *
204
+ * @param ledger - The persisted per-session ledger.
205
+ * @param sessionId - The session id of the current request.
206
+ */
207
+ export const ledgerForSession = (ledger, sessionId) => ledger.sessionId === sessionId ? ledger : freshLedger(sessionId);
208
+ /**
209
+ * Record that an offer for `key` surfaced this session: bump the per-session
210
+ * count and add the key to `offeredTopicKeys` (deduped). Pure; the tool layer
211
+ * persists the result. Called when `get_offer` actually returns an offer so the
212
+ * cadence caps are enforced on the *next* `get_offer`.
213
+ */
214
+ export const markOffered = (ledger, key) => ({
215
+ ...ledger,
216
+ offersThisSession: ledger.offersThisSession + 1,
217
+ offeredTopicKeys: ledger.offeredTopicKeys.includes(key)
218
+ ? ledger.offeredTopicKeys
219
+ : [...ledger.offeredTopicKeys, key],
220
+ });
221
+ /**
222
+ * Merge freshly-matched candidate keys into the per-session ledger's candidate
223
+ * pool (`candidateKeys`) without counting them as offered. `record_observation`
224
+ * uses this to accumulate candidates across the session as signals arrive;
225
+ * `get_offer` (which receives no signals) later resolves from this pool. New
226
+ * candidates are appended in match order, deduped, after the existing pool so
227
+ * the most-relevant-first ordering of a given turn is preserved. Does NOT touch
228
+ * `offeredTopicKeys` or `offersThisSession` — a candidate is not an offer until
229
+ * {@link markOffered}. Pure.
230
+ *
231
+ * @param ledger - The per-session ledger (already reconciled to this session).
232
+ * @param keys - The candidate keys matched this turn (most-relevant first).
233
+ */
234
+ export const noteCandidates = (ledger, keys) => {
235
+ const pool = [...ledger.candidateKeys];
236
+ const seen = new Set(pool);
237
+ for (const key of keys) {
238
+ if (!seen.has(key)) {
239
+ seen.add(key);
240
+ pool.push(key);
241
+ }
242
+ }
243
+ return { ...ledger, candidateKeys: pool };
244
+ };
245
+ /**
246
+ * Apply a decline for `key` (FR-020 within-session + FR-020b cross-session).
247
+ *
248
+ * Within-session (FR-020): set `declinedThisSession = true` so no further offer
249
+ * surfaces for the rest of the session.
250
+ *
251
+ * Cross-session (FR-020b):
252
+ * - increment `consecutiveDeclines`;
253
+ * - push `perTopicNextEligibleAt[key]` out by an exponential backoff:
254
+ * `backoffBaseHours * backoffFactor^(consecutiveDeclines - 1)` hours from
255
+ * `now`, so each successive decline lengthens the re-offer interval;
256
+ * - once `consecutiveDeclines >= declineMuteThreshold`, set a global
257
+ * `mutedUntil` far in the future (offers globally muted until the user
258
+ * re-enables — modeled as a long horizon derived from the backoff).
259
+ *
260
+ * Pure: `now` and config are inputs; the tool layer persists the result.
261
+ *
262
+ * @param ledger - The current per-session ledger (for this session).
263
+ * @param backoff - The current cross-session backoff.
264
+ * @param key - The declined topic key.
265
+ * @param now - The decline instant.
266
+ * @param config - Tunables (decline mute threshold + backoff base/factor).
267
+ * @returns The updated ledger + backoff.
268
+ */
269
+ export const applyDecline = (ledger, backoff, key, now, config = ASSESSMENT_CONFIG) => {
270
+ const consecutiveDeclines = backoff.consecutiveDeclines + 1;
271
+ // Exponential per-topic backoff: base * factor^(n-1) hours from now.
272
+ const backoffHours = config.backoffBaseHours *
273
+ Math.pow(config.backoffFactor, consecutiveDeclines - 1);
274
+ const nextEligibleAt = addHours(now, backoffHours).toISOString();
275
+ const perTopicNextEligibleAt = {
276
+ ...backoff.perTopicNextEligibleAt,
277
+ [key]: nextEligibleAt,
278
+ };
279
+ // Global mute once the threshold is hit (FR-020b). Horizon: a generously long
280
+ // multiple of the current backoff so offers stay muted until the user
281
+ // re-enables / requests one.
282
+ const muted = consecutiveDeclines >= config.declineMuteThreshold;
283
+ const mutedUntil = muted
284
+ ? addHours(now, backoffHours * MUTE_HORIZON_MULTIPLIER).toISOString()
285
+ : backoff.mutedUntil;
286
+ const nextBackoff = {
287
+ consecutiveDeclines,
288
+ perTopicNextEligibleAt,
289
+ ...(mutedUntil !== undefined ? { mutedUntil } : {}),
290
+ };
291
+ const nextLedger = {
292
+ ...ledger,
293
+ declinedThisSession: true,
294
+ };
295
+ return { ledger: nextLedger, backoff: nextBackoff };
296
+ };
297
+ /**
298
+ * Multiplier applied to the current per-topic backoff to derive the global mute
299
+ * horizon when the decline threshold is reached. A large multiple models
300
+ * "muted until the user re-enables" without an unbounded/never-parses date.
301
+ */
302
+ const MUTE_HORIZON_MULTIPLIER = 1000;
303
+ /**
304
+ * Apply an accept (FR-020b): reset `consecutiveDeclines` to 0 and clear the
305
+ * global `mutedUntil`. Per-topic backoff entries are left as-is (an accept on
306
+ * one topic does not retroactively un-back-off unrelated topics, but the global
307
+ * counter/mute reset means the user is engaged again). The per-session ledger is
308
+ * unchanged by an accept. Pure.
309
+ *
310
+ * @param backoff - The current cross-session backoff.
311
+ * @returns The reset backoff.
312
+ */
313
+ export const applyAccept = (backoff) => ({
314
+ consecutiveDeclines: 0,
315
+ perTopicNextEligibleAt: backoff.perTopicNextEligibleAt,
316
+ // mutedUntil intentionally dropped (cleared).
317
+ });
318
+ /**
319
+ * Apply a defer (FR-020): a defer is treated as "ask me later" — it does NOT
320
+ * count as a decline (no backoff increment, no within-session decline flag) and
321
+ * does NOT reset the consecutive-decline counter. The state is returned
322
+ * unchanged so the next end-of-work breakpoint may re-offer normally. Pure.
323
+ */
324
+ export const applyDefer = (ledger, backoff) => ({ ledger, backoff });
325
+ /** Add `hours` to a `Date`, returning a new `Date`. */
326
+ const addHours = (base, hours) => new Date(base.getTime() + hours * 3_600_000);
327
+ //# sourceMappingURL=offers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offers.js","sourceRoot":"","sources":["../../src/observation/offers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @file Observation source abstraction (FR-015/016).
3
+ *
4
+ * Observation is **trigger-only**: a source yields derived, privacy-safe
5
+ * {@link ObservationEvent}s that say *a topic was exercised*, used solely to
6
+ * populate offer candidates. Observation NEVER scores (FR-005, SC-003) and
7
+ * NEVER persists raw prompts, tool inputs, or tool outputs (FR-018, SC-008).
8
+ *
9
+ * The {@link ObservationSource} interface is the seam (FR-016) behind which any
10
+ * provenance can live — a real-time Claude Code hook
11
+ * ({@link ./hookEvents.js#HookSource}), a future transcript-backfill source, or
12
+ * the always-available {@link SelfReportSource} manual path. The rest of the
13
+ * system depends only on this interface, so adding a source needs no redesign.
14
+ *
15
+ * Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018),
16
+ * specs/001-vibe-hero-mvp/data-model.md (§ ObservationEvent),
17
+ * specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
18
+ */
19
+ import { type ObservationEvent } from "../schemas/profile.js";
20
+ import { type AbilityKey, type ToolId } from "../schemas/common.js";
21
+ /**
22
+ * A provenance of derived observation signals (FR-016).
23
+ *
24
+ * Two complementary entry points so every concrete source fits, regardless of
25
+ * how its raw signal arrives:
26
+ *
27
+ * - {@link poll} — for sources that accumulate signals out-of-band (e.g. a hook
28
+ * writing to a buffer, or a transcript reader) and are *drained* on demand.
29
+ * Returns and clears whatever has been observed since the last poll.
30
+ * - {@link record} — for sources handed a single raw payload synchronously
31
+ * (e.g. one hook invocation, or an explicit self-report) and asked to derive
32
+ * events from it immediately.
33
+ *
34
+ * A concrete source MAY implement only the entry point natural to it; the other
35
+ * defaults to yielding nothing. Both return only privacy-safe
36
+ * {@link ObservationEvent}s — never raw payload fields (FR-018).
37
+ */
38
+ export interface ObservationSource {
39
+ /**
40
+ * Stable identifier for the source kind, for diagnostics/telemetry routing.
41
+ * Examples: `"self-report"`, `"claude-code-hook"`, `"transcript-backfill"`.
42
+ */
43
+ readonly kind: string;
44
+ /**
45
+ * Drain accumulated derived events. Implementations that push via
46
+ * {@link record} (or have no buffer) return an empty array.
47
+ *
48
+ * @returns the privacy-safe events observed since the previous poll.
49
+ */
50
+ poll(): Promise<readonly ObservationEvent[]>;
51
+ /**
52
+ * Derive zero or more privacy-safe events from a single raw payload.
53
+ *
54
+ * Implementations MUST treat `raw` as untrusted `unknown`, validate
55
+ * defensively, and copy ONLY derived signals — never `tool_input`,
56
+ * `tool_output`, or any nested raw content (FR-018).
57
+ *
58
+ * @param raw - an untrusted, source-specific payload.
59
+ * @returns the privacy-safe events derived from `raw` (possibly empty).
60
+ */
61
+ record(raw: unknown): readonly ObservationEvent[];
62
+ }
63
+ /**
64
+ * Explicit input for the manual self-report path. The user (or the host agent
65
+ * on their behalf) states outright that one or more topics were exercised; no
66
+ * telemetry is involved, so this path ALWAYS works (FR-016, SC-011).
67
+ */
68
+ export interface SelfReport {
69
+ /** The host tool the activity belongs to. */
70
+ readonly tool: ToolId;
71
+ /**
72
+ * The topic ability keys the user reports exercising (e.g. produced from
73
+ * {@link abilityKey}). At least one is expected; an empty list yields no
74
+ * event.
75
+ */
76
+ readonly topicKeys: readonly AbilityKey[];
77
+ /**
78
+ * Whether the reported activity succeeded. Self-report defaults to `true`
79
+ * (the user is asserting they did the thing); pass `false` to report a
80
+ * failed/abandoned attempt. Note: success is trigger metadata only and never
81
+ * affects scoring (FR-005).
82
+ */
83
+ readonly success?: boolean;
84
+ /**
85
+ * Optional correlation id for this report. Defaults to a generated
86
+ * `self-report:<timestamp>` token; self-report has no upstream id to align
87
+ * with (FR-017 correlation applies to hook↔transcript, not this path).
88
+ */
89
+ readonly correlationId?: string;
90
+ }
91
+ /**
92
+ * Manual, always-available observation source (FR-016, SC-011).
93
+ *
94
+ * Produces an {@link ObservationEvent} from an explicit {@link SelfReport} with
95
+ * no telemetry whatsoever — the canonical fallback when no hook/transcript
96
+ * source is present (Edge: "No telemetry available"). Because the input is
97
+ * already a set of derived `topicKeys`, there is nothing raw to leak; the event
98
+ * is still re-validated against {@link ObservationEventSchema} before it leaves.
99
+ *
100
+ * @example
101
+ * const src = new SelfReportSource();
102
+ * const events = src.record({
103
+ * tool: "claude-code",
104
+ * topicKeys: [abilityKey({ kind: "tool", tool: "claude-code" }, "subagents")],
105
+ * });
106
+ */
107
+ export declare class SelfReportSource implements ObservationSource {
108
+ private readonly now;
109
+ readonly kind = "self-report";
110
+ /**
111
+ * @param now - clock for the event timestamp / default correlation id,
112
+ * injectable for deterministic tests. Defaults to `Date.now`-backed
113
+ * {@link Date}.
114
+ */
115
+ constructor(now?: () => Date);
116
+ /**
117
+ * Self-report is push-only via {@link record}; there is no buffer to drain.
118
+ */
119
+ poll(): Promise<readonly ObservationEvent[]>;
120
+ /**
121
+ * Derive a single {@link ObservationEvent} from an explicit {@link SelfReport}.
122
+ *
123
+ * Returns an empty array when the report names no topics. The returned event
124
+ * carries only derived fields and is schema-validated before return.
125
+ *
126
+ * @param raw - expected to satisfy {@link SelfReport}; validated defensively.
127
+ * @throws {Error} if `raw` is not a well-formed self-report.
128
+ */
129
+ record(raw: unknown): readonly ObservationEvent[];
130
+ /** Defensive structural validation of an untrusted self-report payload. */
131
+ private parseReport;
132
+ }
133
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/observation/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,MAAM,EAAc,MAAM,sBAAsB,CAAC;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,IAAI,IAAI,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC,CAAC;IAE7C;;;;;;;;;OASG;IACH,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,gBAAgB,EAAE,CAAC;CACnD;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,UAAU,EAAE,CAAC;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB;IAQrC,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPvC,SAAgB,IAAI,iBAAiB;IAErC;;;;OAIG;gBACiC,GAAG,GAAE,MAAM,IAAuB;IAEtE;;OAEG;IACI,IAAI,IAAI,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC;IAInD;;;;;;;;OAQG;IACI,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,gBAAgB,EAAE;IAkBxD,2EAA2E;IAC3E,OAAO,CAAC,WAAW;CA0BpB"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file Observation source abstraction (FR-015/016).
3
+ *
4
+ * Observation is **trigger-only**: a source yields derived, privacy-safe
5
+ * {@link ObservationEvent}s that say *a topic was exercised*, used solely to
6
+ * populate offer candidates. Observation NEVER scores (FR-005, SC-003) and
7
+ * NEVER persists raw prompts, tool inputs, or tool outputs (FR-018, SC-008).
8
+ *
9
+ * The {@link ObservationSource} interface is the seam (FR-016) behind which any
10
+ * provenance can live — a real-time Claude Code hook
11
+ * ({@link ./hookEvents.js#HookSource}), a future transcript-backfill source, or
12
+ * the always-available {@link SelfReportSource} manual path. The rest of the
13
+ * system depends only on this interface, so adding a source needs no redesign.
14
+ *
15
+ * Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018),
16
+ * specs/001-vibe-hero-mvp/data-model.md (§ ObservationEvent),
17
+ * specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
18
+ */
19
+ import { ObservationEventSchema, } from "../schemas/profile.js";
20
+ /**
21
+ * Manual, always-available observation source (FR-016, SC-011).
22
+ *
23
+ * Produces an {@link ObservationEvent} from an explicit {@link SelfReport} with
24
+ * no telemetry whatsoever — the canonical fallback when no hook/transcript
25
+ * source is present (Edge: "No telemetry available"). Because the input is
26
+ * already a set of derived `topicKeys`, there is nothing raw to leak; the event
27
+ * is still re-validated against {@link ObservationEventSchema} before it leaves.
28
+ *
29
+ * @example
30
+ * const src = new SelfReportSource();
31
+ * const events = src.record({
32
+ * tool: "claude-code",
33
+ * topicKeys: [abilityKey({ kind: "tool", tool: "claude-code" }, "subagents")],
34
+ * });
35
+ */
36
+ export class SelfReportSource {
37
+ now;
38
+ kind = "self-report";
39
+ /**
40
+ * @param now - clock for the event timestamp / default correlation id,
41
+ * injectable for deterministic tests. Defaults to `Date.now`-backed
42
+ * {@link Date}.
43
+ */
44
+ constructor(now = () => new Date()) {
45
+ this.now = now;
46
+ }
47
+ /**
48
+ * Self-report is push-only via {@link record}; there is no buffer to drain.
49
+ */
50
+ poll() {
51
+ return Promise.resolve([]);
52
+ }
53
+ /**
54
+ * Derive a single {@link ObservationEvent} from an explicit {@link SelfReport}.
55
+ *
56
+ * Returns an empty array when the report names no topics. The returned event
57
+ * carries only derived fields and is schema-validated before return.
58
+ *
59
+ * @param raw - expected to satisfy {@link SelfReport}; validated defensively.
60
+ * @throws {Error} if `raw` is not a well-formed self-report.
61
+ */
62
+ record(raw) {
63
+ const report = this.parseReport(raw);
64
+ if (report.topicKeys.length === 0) {
65
+ return [];
66
+ }
67
+ const timestamp = this.now().toISOString();
68
+ const event = {
69
+ tool: report.tool,
70
+ topicKeys: [...report.topicKeys],
71
+ success: report.success ?? true,
72
+ timestamp,
73
+ correlationId: report.correlationId ?? `self-report:${timestamp}`,
74
+ };
75
+ // Re-validate the derived event; this is the privacy boundary's final
76
+ // checkpoint (the schema has no fields for raw payload content).
77
+ return [ObservationEventSchema.parse(event)];
78
+ }
79
+ /** Defensive structural validation of an untrusted self-report payload. */
80
+ parseReport(raw) {
81
+ if (typeof raw !== "object" || raw === null) {
82
+ throw new Error("SelfReportSource.record: expected a SelfReport object");
83
+ }
84
+ const obj = raw;
85
+ const tool = obj["tool"];
86
+ const topicKeys = obj["topicKeys"];
87
+ if (typeof tool !== "string") {
88
+ throw new Error("SelfReportSource.record: 'tool' must be a ToolId string");
89
+ }
90
+ if (!Array.isArray(topicKeys)) {
91
+ throw new Error("SelfReportSource.record: 'topicKeys' must be an array of AbilityKey");
92
+ }
93
+ const success = obj["success"];
94
+ const correlationId = obj["correlationId"];
95
+ return {
96
+ tool: tool,
97
+ topicKeys: topicKeys,
98
+ ...(typeof success === "boolean" ? { success } : {}),
99
+ ...(typeof correlationId === "string" && correlationId.length > 0
100
+ ? { correlationId }
101
+ : {}),
102
+ };
103
+ }
104
+ }
105
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/observation/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAEL,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AA6E/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,gBAAgB;IAQS;IAPpB,IAAI,GAAG,aAAa,CAAC;IAErC;;;;OAIG;IACH,YAAoC,MAAkB,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;QAAlC,QAAG,GAAH,GAAG,CAA+B;IAAG,CAAC;IAE1E;;OAEG;IACI,IAAI;QACT,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAED;;;;;;;;OAQG;IACI,MAAM,CAAC,GAAY;QACxB,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClC,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAqB;YAC9B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,SAAS,EAAE,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC;YAChC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI;YAC/B,SAAS;YACT,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,eAAe,SAAS,EAAE;SAClE,CAAC;QACF,sEAAsE;QACtE,iEAAiE;QACjE,OAAO,CAAC,sBAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,2EAA2E;IACnE,WAAW,CAAC,GAAY;QAC9B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YAC5C,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM,GAAG,GAAG,GAA8B,CAAC;QAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,CAAC;QACnC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7E,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,qEAAqE,CACtE,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/B,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC;QAC3C,OAAO;YACL,IAAI,EAAE,IAAc;YACpB,SAAS,EAAE,SAAyB;YACpC,GAAG,CAAC,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpD,GAAG,CAAC,OAAO,aAAa,KAAK,QAAQ,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC;gBAC/D,CAAC,CAAC,EAAE,aAAa,EAAE;gBACnB,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;IACJ,CAAC;CACF"}