@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,87 @@
1
+ /**
2
+ * @file Real `record_observation` tool module (T034, US-1).
3
+ *
4
+ * Intake for the observation source (the real-time Claude Code hook in v1). It
5
+ * accepts derived, privacy-safe signals (`toolName` / `mcpTool` / `success` /
6
+ * `toolUseId`) plus a `sessionId`, matches them to candidate topic keys against
7
+ * the catalog's {@link TriggerSignal}s, accumulates the candidates into the
8
+ * per-session {@link OfferLedger}, and returns `{ offerCandidates }`.
9
+ *
10
+ * Trigger-only — AWARDS NOTHING (FR-005 / SC-003). It deliberately touches ONLY
11
+ * the profile's `offers` ledger (its `sessionId` / candidate accounting). It
12
+ * never reads or writes `abilities`, `graduations`, `quizHistory`, or
13
+ * `reviewSchedule`, so observed usage with no answered quiz produces exactly 0
14
+ * change to points or graduation state (SC-003) — the integration test asserts
15
+ * those fields are byte-identical before/after.
16
+ *
17
+ * v1 scope note: a SINGLE real-time hook source feeds this tool. FR-017's
18
+ * two-source (hook + transcript backfill) correlation by `tool_use_id` is
19
+ * architecture-ready (signals carry `toolUseId`; the {@link ObservationSource}
20
+ * seam exists) but is NOT built here, per tasks.md.
21
+ *
22
+ * Gated (FR-032): NOT exempt — `index.ts`/`withSetupGate` returns SETUP_REQUIRED
23
+ * before this handler runs when `profile.config` is absent. The handler assumes
24
+ * a configured profile and reads `config.proactiveOffers` / `config.offerCadence`
25
+ * only to short-circuit candidate work when offers are disabled (so a disabled
26
+ * user never accrues candidate state needlessly).
27
+ *
28
+ * Exposed as a `(dirOverride, catalogLoader)` factory mirroring
29
+ * `startQuiz.ts` / `status.ts`: the registry uses the default instance
30
+ * (env / `~/.vibe-hero` + bundled catalog); tests inject a temp home + fixture
31
+ * catalog.
32
+ *
33
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md
34
+ * (`record_observation`), spec.md FR-005 / FR-015..017 / FR-018 / SC-003,
35
+ * data-model.md (§ OfferLedger).
36
+ */
37
+ import { loadBundledCatalog } from "../catalog/bundled/index.js";
38
+ import { loadProfile, updateProfile } from "../profile/store.js";
39
+ import { RecordObservationInputSchema, } from "../schemas/tools.js";
40
+ import { ledgerForSession, matchCandidates, noteCandidates, } from "../observation/offers.js";
41
+ import { defineTool } from "./types.js";
42
+ /**
43
+ * Build the `record_observation` tool module (US-1).
44
+ *
45
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
46
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
47
+ * bundled snapshot {@link loadBundledCatalog}.
48
+ * @returns The erased registry entry for `record_observation`.
49
+ */
50
+ export const makeRecordObservationTool = (dirOverride, catalogLoader = loadBundledCatalog) => defineTool({
51
+ name: "record_observation",
52
+ description: "Intake derived activity signals and map them to candidate offer topics. Never scores.",
53
+ inputSchema: RecordObservationInputSchema,
54
+ handler: async (input) => {
55
+ const profile = await loadProfile(dirOverride);
56
+ const config = profile.config;
57
+ // If offers are disabled entirely there is nothing to surface — return
58
+ // empty candidates WITHOUT mutating any state (and certainly never
59
+ // touching abilities/graduations/quizHistory — FR-005 / SC-003).
60
+ if (config === undefined ||
61
+ !config.proactiveOffers ||
62
+ config.offerCadence === "off") {
63
+ return { offerCandidates: [] };
64
+ }
65
+ const { topics } = catalogLoader();
66
+ const signals = input.signals.map((s) => ({
67
+ ...(s.toolName !== undefined ? { toolName: s.toolName } : {}),
68
+ ...(s.mcpTool !== undefined ? { mcpTool: s.mcpTool } : {}),
69
+ ...(s.success !== undefined ? { success: s.success } : {}),
70
+ ...(s.toolUseId !== undefined ? { toolUseId: s.toolUseId } : {}),
71
+ }));
72
+ const candidates = matchCandidates(topics, input.tool, signals);
73
+ // Reconcile (or roll over) the per-session ledger, accumulate the matched
74
+ // candidate keys into its per-session pool, and persist ONLY the `offers`
75
+ // block. No scoring fields are read or written — this is the chokepoint
76
+ // that makes SC-003 (usage scores nothing) structurally true.
77
+ const candidateKeys = candidates.map((c) => c.key);
78
+ await updateProfile((current) => ({
79
+ ...current,
80
+ offers: noteCandidates(ledgerForSession(current.offers, input.sessionId), candidateKeys),
81
+ }), dirOverride);
82
+ return { offerCandidates: candidates };
83
+ },
84
+ });
85
+ /** Default `record_observation` module (env / `~/.vibe-hero` + bundled catalog). */
86
+ export const recordObservationTool = makeRecordObservationTool();
87
+ //# sourceMappingURL=recordObservation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordObservation.js","sourceRoot":"","sources":["../../src/tools/recordObservation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,EACL,4BAA4B,GAI7B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,cAAc,GAEf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,UAAU,EAAsB,MAAM,YAAY,CAAC;AAK5D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,WAAoB,EACpB,gBAA+B,kBAAkB,EAClC,EAAE,CACjB,UAAU,CAAC;IACT,IAAI,EAAE,oBAAoB;IAC1B,WAAW,EACT,uFAAuF;IACzF,WAAW,EAAE,4BAA4B;IACzC,OAAO,EAAE,KAAK,EACZ,KAA6B,EACK,EAAE;QACpC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAE9B,uEAAuE;QACvE,mEAAmE;QACnE,iEAAiE;QACjE,IACE,MAAM,KAAK,SAAS;YACpB,CAAC,MAAM,CAAC,eAAe;YACvB,MAAM,CAAC,YAAY,KAAK,KAAK,EAC7B,CAAC;YACD,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE,CAAC;QACjC,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QACnC,MAAM,OAAO,GAAqB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1D,GAAG,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1D,GAAG,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC,CAAC,CAAC;QAEJ,MAAM,UAAU,GAAqB,eAAe,CAClD,MAAM,EACN,KAAK,CAAC,IAAI,EACV,OAAO,CACR,CAAC;QAEF,0EAA0E;QAC1E,0EAA0E;QAC1E,wEAAwE;QACxE,8DAA8D;QAC9D,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACnD,MAAM,aAAa,CACjB,CAAC,OAAgB,EAAW,EAAE,CAAC,CAAC;YAC9B,GAAG,OAAO;YACV,MAAM,EAAE,cAAc,CACpB,gBAAgB,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,EACjD,aAAa,CACd;SACF,CAAC,EACF,WAAW,CACZ,CAAC;QAEF,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;IACzC,CAAC;CACF,CAAC,CAAC;AAEL,oFAAoF;AACpF,MAAM,CAAC,MAAM,qBAAqB,GAAkB,yBAAyB,EAAE,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @file Real `start_quiz` tool module (T031, US-1).
3
+ *
4
+ * Begins a quiz session for one `(topic × class)`: it resolves the topic by key,
5
+ * reads the learner's current ability (cold-start default
6
+ * {@link ASSESSMENT_CONFIG.startingAbility}), computes the next tier boundary
7
+ * above their current tier, and asks the pure {@link selectItems} engine to pick
8
+ * a small bounded set (3–5, default {@link ASSESSMENT_CONFIG.defaultQuizLength})
9
+ * of difficulty-targeted items — excluding the items most recently served
10
+ * (`abilities[key].lastItemIds`) so a session never re-serves the same item
11
+ * back-to-back (edge case "repeated identical questions").
12
+ *
13
+ * It then creates a {@link QuizRecord} (`startedAt`, NO `completedAt` — a partial
14
+ * session must never count toward graduation, FR-008a) and persists it via the
15
+ * store's atomic {@link updateProfile}. The returned `items` are
16
+ * {@link PresentedItem}s with **answer keys STRIPPED** for deterministic types —
17
+ * the engine must never leak the correct answer before grading (contract;
18
+ * SC-004 reproducibility relies on grading server-side, not the client knowing
19
+ * the key).
20
+ *
21
+ * Free-form (v1, T048/T049): `start_quiz` now SUPPORTS `free_form` items in
22
+ * selection. A presented `free_form` item carries `rubric.criteria` (with ids)
23
+ * AND `referenceAnswer` so the host agent can run the judging handshake
24
+ * (`submit_answer` free-form path, T048). Deterministic items still strip every
25
+ * answer key — the engine grades them server-side (SC-004).
26
+ *
27
+ * Graceful degradation (FR-014, T049): when free-form judging is UNAVAILABLE —
28
+ * the host agent can't judge an open answer — the caller passes
29
+ * `allowFreeForm: false`. Selection then EXCLUDES `free_form` items, preferring
30
+ * deterministic ones so the quiz still completes rather than serving an
31
+ * unjudgeable item. The flag defaults to `true` (judging available); a quiz
32
+ * therefore never gets stuck with only an unjudgeable item — if the topic has
33
+ * deterministic items they are served instead.
34
+ *
35
+ * Gated (FR-032): NOT exempt — `index.ts`/`withSetupGate` returns SETUP_REQUIRED
36
+ * before this handler runs when `profile.config` is absent.
37
+ *
38
+ * Exposed as a `dirOverride`-closing factory mirroring `config.ts` / `status.ts`:
39
+ * the registry uses the default instance (env / `~/.vibe-hero`); tests build a
40
+ * dir-scoped instance against a temp home.
41
+ *
42
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`start_quiz`),
43
+ * spec.md FR-008a / FR-011 / FR-018, data-model.md (QuizRecord), research.md
44
+ * (OD-005 selection).
45
+ */
46
+ import type { CatalogLoadResult } from "../catalog/loader.js";
47
+ import { type AnyToolModule } from "./types.js";
48
+ /**
49
+ * Compute the difficulty of the next tier boundary ABOVE a learner at
50
+ * `currentTier`, which the selection engine clamps its difficulty target to (the
51
+ * "promotion bar"). The boundaries sit halfway between tier centers
52
+ * (`tierBoundaries = [150, 250, 350, 450]`):
53
+ * - not yet graduated (tier 0) or at tier 100 ⇒ the first boundary (150).
54
+ * - tier 200 ⇒ 250, tier 300 ⇒ 350, tier 400 ⇒ 450.
55
+ * - tier 500 (top) ⇒ the last boundary (450); there is no higher bar.
56
+ *
57
+ * Indexing: boundary `i` lies just above tier center `i` (100→[0], 200→[1] …),
58
+ * so the next boundary above tier `t` is `tierBoundaries[centerIndex(t)]`,
59
+ * clamped to the last boundary at the top tier.
60
+ *
61
+ * @param currentTier - The learner's current tier (0 = not yet graduated).
62
+ * @returns The difficulty of the next boundary to target.
63
+ */
64
+ export declare const nextBoundaryFor: (currentTier: number) => number;
65
+ /**
66
+ * A catalog source: returns the loaded topics (+ any per-file errors). Defaults
67
+ * to {@link loadBundledCatalog}; tests inject a fixture-dir loader so the US-1
68
+ * loop can exercise a topic with ≥5 deterministic items without disturbing the
69
+ * shared bundled snapshot.
70
+ */
71
+ export type CatalogLoader = () => CatalogLoadResult;
72
+ /**
73
+ * Build the `start_quiz` tool module (US-1).
74
+ *
75
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
76
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
77
+ * bundled snapshot {@link loadBundledCatalog}.
78
+ */
79
+ export declare const makeStartQuizTool: (dirOverride?: string, catalogLoader?: CatalogLoader) => AnyToolModule;
80
+ /** Default `start_quiz` module (env / `~/.vibe-hero`), used by the registry. */
81
+ export declare const startQuizTool: AnyToolModule;
82
+ //# sourceMappingURL=startQuiz.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startQuiz.d.ts","sourceRoot":"","sources":["../../src/tools/startQuiz.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAY9D,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAS5D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,GAAI,aAAa,MAAM,KAAG,MAUrD,CAAC;AAkCF;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC5B,cAAc,MAAM,EACpB,gBAAe,aAAkC,KAChD,aAsEC,CAAC;AAEL,gFAAgF;AAChF,eAAO,MAAM,aAAa,EAAE,aAAmC,CAAC"}
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @file Real `start_quiz` tool module (T031, US-1).
3
+ *
4
+ * Begins a quiz session for one `(topic × class)`: it resolves the topic by key,
5
+ * reads the learner's current ability (cold-start default
6
+ * {@link ASSESSMENT_CONFIG.startingAbility}), computes the next tier boundary
7
+ * above their current tier, and asks the pure {@link selectItems} engine to pick
8
+ * a small bounded set (3–5, default {@link ASSESSMENT_CONFIG.defaultQuizLength})
9
+ * of difficulty-targeted items — excluding the items most recently served
10
+ * (`abilities[key].lastItemIds`) so a session never re-serves the same item
11
+ * back-to-back (edge case "repeated identical questions").
12
+ *
13
+ * It then creates a {@link QuizRecord} (`startedAt`, NO `completedAt` — a partial
14
+ * session must never count toward graduation, FR-008a) and persists it via the
15
+ * store's atomic {@link updateProfile}. The returned `items` are
16
+ * {@link PresentedItem}s with **answer keys STRIPPED** for deterministic types —
17
+ * the engine must never leak the correct answer before grading (contract;
18
+ * SC-004 reproducibility relies on grading server-side, not the client knowing
19
+ * the key).
20
+ *
21
+ * Free-form (v1, T048/T049): `start_quiz` now SUPPORTS `free_form` items in
22
+ * selection. A presented `free_form` item carries `rubric.criteria` (with ids)
23
+ * AND `referenceAnswer` so the host agent can run the judging handshake
24
+ * (`submit_answer` free-form path, T048). Deterministic items still strip every
25
+ * answer key — the engine grades them server-side (SC-004).
26
+ *
27
+ * Graceful degradation (FR-014, T049): when free-form judging is UNAVAILABLE —
28
+ * the host agent can't judge an open answer — the caller passes
29
+ * `allowFreeForm: false`. Selection then EXCLUDES `free_form` items, preferring
30
+ * deterministic ones so the quiz still completes rather than serving an
31
+ * unjudgeable item. The flag defaults to `true` (judging available); a quiz
32
+ * therefore never gets stuck with only an unjudgeable item — if the topic has
33
+ * deterministic items they are served instead.
34
+ *
35
+ * Gated (FR-032): NOT exempt — `index.ts`/`withSetupGate` returns SETUP_REQUIRED
36
+ * before this handler runs when `profile.config` is absent.
37
+ *
38
+ * Exposed as a `dirOverride`-closing factory mirroring `config.ts` / `status.ts`:
39
+ * the registry uses the default instance (env / `~/.vibe-hero`); tests build a
40
+ * dir-scoped instance against a temp home.
41
+ *
42
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`start_quiz`),
43
+ * spec.md FR-008a / FR-011 / FR-018, data-model.md (QuizRecord), research.md
44
+ * (OD-005 selection).
45
+ */
46
+ import { randomUUID } from "node:crypto";
47
+ import { ASSESSMENT_CONFIG } from "../config.js";
48
+ import { loadBundledCatalog } from "../catalog/bundled/index.js";
49
+ import { selectItems } from "../engine/selection.js";
50
+ import { loadProfile, updateProfile } from "../profile/store.js";
51
+ import { abilityKey, parseAbilityKey } from "../schemas/common.js";
52
+ import { StartQuizInputSchema, } from "../schemas/tools.js";
53
+ import { defineTool } from "./types.js";
54
+ /** Find the catalog topic whose `(class, id)` serializes to `key`. */
55
+ const findTopicByKey = (topics, key) => topics.find((topic) => abilityKey(topic.class, topic.id) === key);
56
+ /**
57
+ * Compute the difficulty of the next tier boundary ABOVE a learner at
58
+ * `currentTier`, which the selection engine clamps its difficulty target to (the
59
+ * "promotion bar"). The boundaries sit halfway between tier centers
60
+ * (`tierBoundaries = [150, 250, 350, 450]`):
61
+ * - not yet graduated (tier 0) or at tier 100 ⇒ the first boundary (150).
62
+ * - tier 200 ⇒ 250, tier 300 ⇒ 350, tier 400 ⇒ 450.
63
+ * - tier 500 (top) ⇒ the last boundary (450); there is no higher bar.
64
+ *
65
+ * Indexing: boundary `i` lies just above tier center `i` (100→[0], 200→[1] …),
66
+ * so the next boundary above tier `t` is `tierBoundaries[centerIndex(t)]`,
67
+ * clamped to the last boundary at the top tier.
68
+ *
69
+ * @param currentTier - The learner's current tier (0 = not yet graduated).
70
+ * @returns The difficulty of the next boundary to target.
71
+ */
72
+ export const nextBoundaryFor = (currentTier) => {
73
+ const { tierCenters, tierBoundaries } = ASSESSMENT_CONFIG;
74
+ // Tier 0 (ungraduated) behaves like the bottom rung: aim at the first boundary.
75
+ if (currentTier <= tierCenters[0])
76
+ return tierBoundaries[0];
77
+ const centerIndex = tierCenters.indexOf(currentTier);
78
+ // Unknown tier (defensive) or the top tier: clamp to the last boundary.
79
+ if (centerIndex < 0 || centerIndex >= tierBoundaries.length) {
80
+ return tierBoundaries[tierBoundaries.length - 1];
81
+ }
82
+ return tierBoundaries[centerIndex];
83
+ };
84
+ /**
85
+ * Strip an item to its presented form. Deterministic items (`multiple_choice` /
86
+ * `short_answer`) carry NO answer key and NO rubric/referenceAnswer — the host
87
+ * agent and user must never see the correct answer before grading. MC keeps its
88
+ * `choices` (without marking the correct one). `free_form` items carry
89
+ * `rubric.criteria` (ids + text) + `referenceAnswer` so the host agent can run
90
+ * the judging handshake (T048) — the rubric here is the agent's INSTRUCTION, not
91
+ * a leaked deterministic key.
92
+ */
93
+ const toPresentedItem = (item) => {
94
+ const base = {
95
+ itemId: item.id,
96
+ tier: item.tier,
97
+ type: item.type,
98
+ prompt: item.prompt,
99
+ };
100
+ if (item.type === "multiple_choice") {
101
+ // Present choices WITHOUT the answer key — the correct id never leaves here.
102
+ return { ...base, choices: item.choices };
103
+ }
104
+ if (item.type === "free_form" && item.rubric !== undefined) {
105
+ // Judging handshake (T048): hand the agent criteria + reference answer.
106
+ return {
107
+ ...base,
108
+ rubric: { criteria: item.rubric.criteria },
109
+ referenceAnswer: item.rubric.referenceAnswer,
110
+ };
111
+ }
112
+ // short_answer: prompt only, no answer key leaked.
113
+ return base;
114
+ };
115
+ /**
116
+ * Build the `start_quiz` tool module (US-1).
117
+ *
118
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
119
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
120
+ * bundled snapshot {@link loadBundledCatalog}.
121
+ */
122
+ export const makeStartQuizTool = (dirOverride, catalogLoader = loadBundledCatalog) => defineTool({
123
+ name: "start_quiz",
124
+ description: "Begin a quiz session for a topic, selecting 3-5 difficulty-targeted items.",
125
+ inputSchema: StartQuizInputSchema,
126
+ handler: async (input) => {
127
+ const key = input.key;
128
+ // Validate the key shape (throws on malformed) before any lookup.
129
+ parseAbilityKey(key);
130
+ const profile = await loadProfile(dirOverride);
131
+ const { topics } = catalogLoader();
132
+ const topic = findTopicByKey(topics, key);
133
+ if (topic === undefined) {
134
+ throw new Error(`start_quiz: no catalog topic matches key ${JSON.stringify(key)}`);
135
+ }
136
+ const estimate = profile.abilities[key];
137
+ const ability = estimate?.value ?? ASSESSMENT_CONFIG.startingAbility;
138
+ const currentTier = profile.graduations[key]?.currentTier ?? 0;
139
+ // Graceful degradation (FR-014, T049): free-form items are eligible only
140
+ // when the host can judge them (`allowFreeForm`, default true). When
141
+ // judging is unavailable the caller passes `false` and we EXCLUDE
142
+ // `free_form`, leaving the deterministic backbone so the quiz still
143
+ // completes rather than serving an unjudgeable item.
144
+ const allowFreeForm = input.allowFreeForm ?? true;
145
+ const candidates = allowFreeForm
146
+ ? topic.items
147
+ : topic.items.filter((item) => item.type !== "free_form");
148
+ const length = input.length ?? ASSESSMENT_CONFIG.defaultQuizLength;
149
+ const selected = selectItems({
150
+ ability,
151
+ candidates,
152
+ nextBoundary: nextBoundaryFor(currentTier),
153
+ recentItemIds: estimate?.lastItemIds ?? [],
154
+ length,
155
+ });
156
+ const quizId = randomUUID();
157
+ const startedAt = new Date().toISOString();
158
+ const record = {
159
+ id: quizId,
160
+ key,
161
+ startedAt,
162
+ // NO completedAt — partial sessions never count toward graduation.
163
+ items: [],
164
+ abilityBefore: ability,
165
+ // Unchanged until items are graded; submit_answer advances it.
166
+ abilityAfter: ability,
167
+ };
168
+ await updateProfile((current) => ({
169
+ ...current,
170
+ quizHistory: [...current.quizHistory, record],
171
+ }), dirOverride);
172
+ return {
173
+ quizId,
174
+ items: selected.map(toPresentedItem),
175
+ };
176
+ },
177
+ });
178
+ /** Default `start_quiz` module (env / `~/.vibe-hero`), used by the registry. */
179
+ export const startQuizTool = makeStartQuizTool();
180
+ //# sourceMappingURL=startQuiz.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startQuiz.js","sourceRoot":"","sources":["../../src/tools/startQuiz.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @file Real `get_status` tool module (T025, US-2).
3
+ *
4
+ * Reports the learner's standing for a tool (or, when no `tool` is given, the
5
+ * first tool they are learning per `config.toolsLearning`). For every in-scope
6
+ * catalog topic it returns a per-(topic × class) row — key, title, displayed
7
+ * tier, status (`current` / `due_for_review` / `not_started`), and current
8
+ * ability — plus a `dueForReview` key list and weakest-first `suggestions`.
9
+ *
10
+ * Telemetry-free (SC-011): the result is derived purely from the bundled catalog
11
+ * and the profile's `abilities`/`graduations`. No observation, offer, or
12
+ * transcript state is consulted, so status works fully in the pull path even
13
+ * when no hook is installed (FR-021).
14
+ *
15
+ * Lapse (T046, US-3 / FR-009): on read, graduated topics that have gone stale
16
+ * and decayed near/under their lower band ({@link detectLapses}, OD-003) are
17
+ * surfaced as `due_for_review`. This is the read-side trigger for knowledge
18
+ * lapse: status reads the clock and passes `now` into the PURE lapse engine,
19
+ * then persists the `due_for_review` status and enqueues a
20
+ * `ReviewEntry{reason:"lapsed"}` for each newly-detected topic (T046 owns
21
+ * reviewSchedule writes for BOTH the spaced — on graduation — and lapsed reasons,
22
+ * resolving analyze C1's single-writer concern). The enqueue is idempotent: a
23
+ * topic already flagged `due_for_review` is skipped, so re-reading status does
24
+ * not pile up duplicate lapsed entries.
25
+ *
26
+ * Gated (FR-032): this tool is NOT exempt, so `index.ts`/`withSetupGate` returns
27
+ * SETUP_REQUIRED before the handler runs when `profile.config` is absent. The
28
+ * handler therefore assumes a configured profile and reads `config.toolsLearning`
29
+ * to resolve the default tool.
30
+ *
31
+ * Each tool is exposed as a factory closing over an optional `dirOverride` (the
32
+ * store's test seam), mirroring `config.ts`: the registry uses the default
33
+ * instance (env / `~/.vibe-hero`); tests build a dir-scoped instance against a
34
+ * temp home.
35
+ *
36
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`get_status`),
37
+ * spec.md US-2 / FR-021 / SC-011.
38
+ */
39
+ import type { CatalogLoadResult } from "../catalog/loader.js";
40
+ import { type AnyToolModule } from "./types.js";
41
+ /**
42
+ * A catalog source: returns the loaded topics (+ any per-file errors). Defaults
43
+ * to {@link loadBundledCatalog}; tests inject a fixture-dir loader so lapse
44
+ * detection can resolve a graduated topic without populating the shared bundled
45
+ * snapshot (mirrors the `start_quiz` / `submit_answer` seam).
46
+ */
47
+ export type CatalogLoader = () => CatalogLoadResult;
48
+ /**
49
+ * Build the `get_status` tool module (US-2).
50
+ *
51
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
52
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
53
+ * bundled snapshot {@link loadBundledCatalog}.
54
+ * @returns The erased registry entry for `get_status`.
55
+ */
56
+ export declare const makeGetStatusTool: (dirOverride?: string, catalogLoader?: CatalogLoader) => AnyToolModule;
57
+ /** Default `get_status` module (env / `~/.vibe-hero`), used by the registry. */
58
+ export declare const getStatusTool: AnyToolModule;
59
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAK9D,OAAO,EAAc,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAmE5D;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,iBAAiB,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,cAAc,MAAM,EACpB,gBAAe,aAAkC,KAChD,aAwCC,CAAC;AAEL,gFAAgF;AAChF,eAAO,MAAM,aAAa,EAAE,aAAmC,CAAC"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @file Real `get_status` tool module (T025, US-2).
3
+ *
4
+ * Reports the learner's standing for a tool (or, when no `tool` is given, the
5
+ * first tool they are learning per `config.toolsLearning`). For every in-scope
6
+ * catalog topic it returns a per-(topic × class) row — key, title, displayed
7
+ * tier, status (`current` / `due_for_review` / `not_started`), and current
8
+ * ability — plus a `dueForReview` key list and weakest-first `suggestions`.
9
+ *
10
+ * Telemetry-free (SC-011): the result is derived purely from the bundled catalog
11
+ * and the profile's `abilities`/`graduations`. No observation, offer, or
12
+ * transcript state is consulted, so status works fully in the pull path even
13
+ * when no hook is installed (FR-021).
14
+ *
15
+ * Lapse (T046, US-3 / FR-009): on read, graduated topics that have gone stale
16
+ * and decayed near/under their lower band ({@link detectLapses}, OD-003) are
17
+ * surfaced as `due_for_review`. This is the read-side trigger for knowledge
18
+ * lapse: status reads the clock and passes `now` into the PURE lapse engine,
19
+ * then persists the `due_for_review` status and enqueues a
20
+ * `ReviewEntry{reason:"lapsed"}` for each newly-detected topic (T046 owns
21
+ * reviewSchedule writes for BOTH the spaced — on graduation — and lapsed reasons,
22
+ * resolving analyze C1's single-writer concern). The enqueue is idempotent: a
23
+ * topic already flagged `due_for_review` is skipped, so re-reading status does
24
+ * not pile up duplicate lapsed entries.
25
+ *
26
+ * Gated (FR-032): this tool is NOT exempt, so `index.ts`/`withSetupGate` returns
27
+ * SETUP_REQUIRED before the handler runs when `profile.config` is absent. The
28
+ * handler therefore assumes a configured profile and reads `config.toolsLearning`
29
+ * to resolve the default tool.
30
+ *
31
+ * Each tool is exposed as a factory closing over an optional `dirOverride` (the
32
+ * store's test seam), mirroring `config.ts`: the registry uses the default
33
+ * instance (env / `~/.vibe-hero`); tests build a dir-scoped instance against a
34
+ * temp home.
35
+ *
36
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md (`get_status`),
37
+ * spec.md US-2 / FR-021 / SC-011.
38
+ */
39
+ import { loadBundledCatalog } from "../catalog/bundled/index.js";
40
+ import { loadProfile, updateProfile } from "../profile/store.js";
41
+ import { defineTool } from "./types.js";
42
+ import { GetStatusInputSchema, } from "../schemas/tools.js";
43
+ import { computeStandings, detectLapses, rankByWeakness, suggestionReason, } from "./us2/standing.js";
44
+ /**
45
+ * Resolve which tool `get_status` reports on. Honors an explicit `tool`; else
46
+ * falls back to the first tool the user is learning; else `claude-code` (the
47
+ * only tool v1 ships content for). The handler is gated, so `config` is present.
48
+ */
49
+ const resolveTool = (requested, toolsLearning) => requested ?? toolsLearning[0] ?? "claude-code";
50
+ /** How many weakest/stale topics to surface as `suggestions`. */
51
+ const MAX_SUGGESTIONS = 3;
52
+ /**
53
+ * Persist a detected knowledge lapse (T046 / FR-009): for each newly-lapsed key,
54
+ * flag its graduation `due_for_review` and enqueue a `ReviewEntry{reason:
55
+ * "lapsed"}` due now. Runs inside one atomic {@link updateProfile} transaction
56
+ * (FR-023a). Re-derives the lapse status from the CURRENT on-disk profile so a
57
+ * concurrent writer that already flagged/handled a key is respected (idempotent:
58
+ * an already-`due_for_review` key is skipped, and a `lapsed` entry is not
59
+ * duplicated). No-op when `newlyLapsed` is empty.
60
+ *
61
+ * @param newlyLapsed - Keys that {@link detectLapses} found newly due.
62
+ * @param now - The reference ISO datetime (the lapsed entry's `dueAt`).
63
+ * @param dirOverride - Profile-directory override (test seam).
64
+ */
65
+ const persistLapses = async (newlyLapsed, now, dirOverride) => {
66
+ if (newlyLapsed.length === 0)
67
+ return;
68
+ await updateProfile((current) => {
69
+ const graduations = { ...current.graduations };
70
+ const reviewSchedule = [...current.reviewSchedule];
71
+ for (const key of newlyLapsed) {
72
+ const grad = graduations[key];
73
+ // Skip if it vanished or a concurrent writer already flagged it.
74
+ if (grad === undefined || grad.status === "due_for_review")
75
+ continue;
76
+ graduations[key] = {
77
+ ...grad,
78
+ status: "due_for_review",
79
+ lastChangeReason: "review_due",
80
+ };
81
+ const already = reviewSchedule.some((e) => e.key === key && e.reason === "lapsed");
82
+ if (!already) {
83
+ const entry = { key, dueAt: now, reason: "lapsed" };
84
+ reviewSchedule.push(entry);
85
+ }
86
+ }
87
+ return { ...current, graduations, reviewSchedule };
88
+ }, dirOverride);
89
+ };
90
+ /**
91
+ * Build the `get_status` tool module (US-2).
92
+ *
93
+ * @param dirOverride - Profile-directory override (test seam); see `profileDir`.
94
+ * @param catalogLoader - Catalog source override (test seam); defaults to the
95
+ * bundled snapshot {@link loadBundledCatalog}.
96
+ * @returns The erased registry entry for `get_status`.
97
+ */
98
+ export const makeGetStatusTool = (dirOverride, catalogLoader = loadBundledCatalog) => defineTool({
99
+ name: "get_status",
100
+ description: "Show the user's learning standing for a tool (or all). Read-only.",
101
+ inputSchema: GetStatusInputSchema,
102
+ handler: async (input) => {
103
+ const profile = await loadProfile(dirOverride);
104
+ const tool = resolveTool(input.tool, profile.config?.toolsLearning ?? []);
105
+ // Bundled catalog is always available offline (FR-025); malformed files
106
+ // are reported as `errors` and simply skipped here — status still lists
107
+ // every topic that loaded cleanly.
108
+ const { topics } = catalogLoader();
109
+ const baseStandings = computeStandings(topics, profile, tool);
110
+ // Lapse detection (T046 / FR-009): read the clock once and pass it into
111
+ // the PURE lapse engine. Graduated topics that have gone stale + decayed
112
+ // near their lower band are surfaced as due_for_review; persist the
113
+ // status + a `lapsed` review entry so the lapse durably surfaces.
114
+ const now = new Date().toISOString();
115
+ const { standings, newlyLapsed } = detectLapses(baseStandings, profile, now);
116
+ await persistLapses(newlyLapsed, now, dirOverride);
117
+ const dueForReview = standings
118
+ .filter((s) => s.row.status === "due_for_review")
119
+ .map((s) => s.key);
120
+ const suggestions = rankByWeakness(standings)
121
+ .slice(0, MAX_SUGGESTIONS)
122
+ .map((s) => ({ key: s.key, reason: suggestionReason(s) }));
123
+ return {
124
+ tool,
125
+ topics: standings.map((s) => s.row),
126
+ dueForReview,
127
+ suggestions,
128
+ };
129
+ },
130
+ });
131
+ /** Default `get_status` module (env / `~/.vibe-hero`), used by the registry. */
132
+ export const getStatusTool = makeGetStatusTool();
133
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/tools/status.ts"],"names":[],"mappings":"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"}