@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.
- package/LICENSE +190 -0
- package/README.md +151 -0
- package/dist/catalog/bundled/claude-code/.gitkeep +0 -0
- package/dist/catalog/bundled/claude-code/context-management.yaml +302 -0
- package/dist/catalog/bundled/claude-code/planning.yaml +313 -0
- package/dist/catalog/bundled/claude-code/subagents.yaml +357 -0
- package/dist/catalog/bundled/general/.gitkeep +0 -0
- package/dist/catalog/bundled/general/_placeholder.yaml +39 -0
- package/dist/catalog/bundled/general/task-decomposition.yaml +390 -0
- package/dist/catalog/bundled/index.d.ts +39 -0
- package/dist/catalog/bundled/index.d.ts.map +1 -0
- package/dist/catalog/bundled/index.js +41 -0
- package/dist/catalog/bundled/index.js.map +1 -0
- package/dist/catalog/fetcher.d.ts +201 -0
- package/dist/catalog/fetcher.d.ts.map +1 -0
- package/dist/catalog/fetcher.js +452 -0
- package/dist/catalog/fetcher.js.map +1 -0
- package/dist/catalog/loader.d.ts +165 -0
- package/dist/catalog/loader.d.ts.map +1 -0
- package/dist/catalog/loader.js +241 -0
- package/dist/catalog/loader.js.map +1 -0
- package/dist/catalog/resolve.d.ts +85 -0
- package/dist/catalog/resolve.d.ts.map +1 -0
- package/dist/catalog/resolve.js +103 -0
- package/dist/catalog/resolve.js.map +1 -0
- package/dist/cli/getOffer.d.ts +38 -0
- package/dist/cli/getOffer.d.ts.map +1 -0
- package/dist/cli/getOffer.js +150 -0
- package/dist/cli/getOffer.js.map +1 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +88 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +63 -0
- package/dist/config.js.map +1 -0
- package/dist/engine/elo.d.ts +76 -0
- package/dist/engine/elo.d.ts.map +1 -0
- package/dist/engine/elo.js +79 -0
- package/dist/engine/elo.js.map +1 -0
- package/dist/engine/graduation.d.ts +108 -0
- package/dist/engine/graduation.d.ts.map +1 -0
- package/dist/engine/graduation.js +161 -0
- package/dist/engine/graduation.js.map +1 -0
- package/dist/engine/lapse.d.ts +80 -0
- package/dist/engine/lapse.d.ts.map +1 -0
- package/dist/engine/lapse.js +125 -0
- package/dist/engine/lapse.js.map +1 -0
- package/dist/engine/selection.d.ts +84 -0
- package/dist/engine/selection.d.ts.map +1 -0
- package/dist/engine/selection.js +119 -0
- package/dist/engine/selection.js.map +1 -0
- package/dist/grading/deterministic.d.ts +102 -0
- package/dist/grading/deterministic.d.ts.map +1 -0
- package/dist/grading/deterministic.js +118 -0
- package/dist/grading/deterministic.js.map +1 -0
- package/dist/grading/freeform.d.ts +64 -0
- package/dist/grading/freeform.d.ts.map +1 -0
- package/dist/grading/freeform.js +85 -0
- package/dist/grading/freeform.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/observation/hookEvents.d.ts +113 -0
- package/dist/observation/hookEvents.d.ts.map +1 -0
- package/dist/observation/hookEvents.js +170 -0
- package/dist/observation/hookEvents.js.map +1 -0
- package/dist/observation/offers.d.ts +215 -0
- package/dist/observation/offers.d.ts.map +1 -0
- package/dist/observation/offers.js +327 -0
- package/dist/observation/offers.js.map +1 -0
- package/dist/observation/source.d.ts +133 -0
- package/dist/observation/source.d.ts.map +1 -0
- package/dist/observation/source.js +105 -0
- package/dist/observation/source.js.map +1 -0
- package/dist/profile/migrate.d.ts +122 -0
- package/dist/profile/migrate.d.ts.map +1 -0
- package/dist/profile/migrate.js +147 -0
- package/dist/profile/migrate.js.map +1 -0
- package/dist/profile/store.d.ts +84 -0
- package/dist/profile/store.d.ts.map +1 -0
- package/dist/profile/store.js +267 -0
- package/dist/profile/store.js.map +1 -0
- package/dist/schemas/common.d.ts +95 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +106 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/content.d.ts +828 -0
- package/dist/schemas/content.d.ts.map +1 -0
- package/dist/schemas/content.js +219 -0
- package/dist/schemas/content.js.map +1 -0
- package/dist/schemas/profile.d.ts +599 -0
- package/dist/schemas/profile.d.ts.map +1 -0
- package/dist/schemas/profile.js +177 -0
- package/dist/schemas/profile.js.map +1 -0
- package/dist/schemas/tools.d.ts +1581 -0
- package/dist/schemas/tools.d.ts.map +1 -0
- package/dist/schemas/tools.js +286 -0
- package/dist/schemas/tools.js.map +1 -0
- package/dist/tools/config.d.ts +51 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +104 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/gate.d.ts +50 -0
- package/dist/tools/gate.d.ts.map +1 -0
- package/dist/tools/gate.js +67 -0
- package/dist/tools/gate.js.map +1 -0
- package/dist/tools/guidance.d.ts +36 -0
- package/dist/tools/guidance.d.ts.map +1 -0
- package/dist/tools/guidance.js +117 -0
- package/dist/tools/guidance.js.map +1 -0
- package/dist/tools/listTopics.d.ts +55 -0
- package/dist/tools/listTopics.d.ts.map +1 -0
- package/dist/tools/listTopics.js +78 -0
- package/dist/tools/listTopics.js.map +1 -0
- package/dist/tools/offers.d.ts +60 -0
- package/dist/tools/offers.d.ts.map +1 -0
- package/dist/tools/offers.js +152 -0
- package/dist/tools/offers.js.map +1 -0
- package/dist/tools/placeholders.d.ts +27 -0
- package/dist/tools/placeholders.d.ts.map +1 -0
- package/dist/tools/placeholders.js +49 -0
- package/dist/tools/placeholders.js.map +1 -0
- package/dist/tools/recordObservation.d.ts +52 -0
- package/dist/tools/recordObservation.d.ts.map +1 -0
- package/dist/tools/recordObservation.js +87 -0
- package/dist/tools/recordObservation.js.map +1 -0
- package/dist/tools/startQuiz.d.ts +82 -0
- package/dist/tools/startQuiz.d.ts.map +1 -0
- package/dist/tools/startQuiz.js +180 -0
- package/dist/tools/startQuiz.js.map +1 -0
- package/dist/tools/status.d.ts +59 -0
- package/dist/tools/status.d.ts.map +1 -0
- package/dist/tools/status.js +133 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/submitAnswer.d.ts +156 -0
- package/dist/tools/submitAnswer.d.ts.map +1 -0
- package/dist/tools/submitAnswer.js +402 -0
- package/dist/tools/submitAnswer.js.map +1 -0
- package/dist/tools/types.d.ts +82 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +48 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/us2/standing.d.ts +111 -0
- package/dist/tools/us2/standing.d.ts.map +1 -0
- package/dist/tools/us2/standing.js +143 -0
- package/dist/tools/us2/standing.js.map +1 -0
- 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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIjE,OAAO,EAAE,UAAU,EAAsB,MAAM,YAAY,CAAC;AAC5D,OAAO,EACL,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,MAAM,WAAW,GAAG,CAClB,SAA6B,EAC7B,aAAgC,EACxB,EAAE,CAAC,SAAS,IAAI,aAAa,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC;AAE5D,iEAAiE;AACjE,MAAM,eAAe,GAAG,CAAC,CAAC;AAE1B;;;;;;;;;;;;GAYG;AACH,MAAM,aAAa,GAAG,KAAK,EACzB,WAAkC,EAClC,GAAW,EACX,WAA+B,EAChB,EAAE;IACjB,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IACrC,MAAM,aAAa,CAAC,CAAC,OAAgB,EAAW,EAAE;QAChD,MAAM,WAAW,GAAG,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAC/C,MAAM,cAAc,GAAG,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;QACnD,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;YAC9B,iEAAiE;YACjE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,gBAAgB;gBAAE,SAAS;YACrE,WAAW,CAAC,GAAG,CAAC,GAAG;gBACjB,GAAG,IAAI;gBACP,MAAM,EAAE,gBAAgB;gBACxB,gBAAgB,EAAE,YAAY;aAC/B,CAAC;YACF,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CACjC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAC9C,CAAC;YACF,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,KAAK,GAAgB,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;gBACjE,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,OAAO,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC;IACrD,CAAC,EAAE,WAAW,CAAC,CAAC;AAClB,CAAC,CAAC;AAUF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,WAAoB,EACpB,gBAA+B,kBAAkB,EAClC,EAAE,CACjB,UAAU,CAAC;IACT,IAAI,EAAE,YAAY;IAClB,WAAW,EACT,mEAAmE;IACrE,WAAW,EAAE,oBAAoB;IACjC,OAAO,EAAE,KAAK,EAAE,KAAqB,EAA4B,EAAE;QACjE,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,WAAW,CAAC,CAAC;QAC/C,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,aAAa,IAAI,EAAE,CAAC,CAAC;QAE1E,wEAAwE;QACxE,wEAAwE;QACxE,mCAAmC;QACnC,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,EAAE,CAAC;QAEnC,MAAM,aAAa,GAAG,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;QAE9D,wEAAwE;QACxE,yEAAyE;QACzE,oEAAoE;QACpE,kEAAkE;QAClE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,GAAG,YAAY,CAAC,aAAa,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAC7E,MAAM,aAAa,CAAC,WAAW,EAAE,GAAG,EAAE,WAAW,CAAC,CAAC;QAEnD,MAAM,YAAY,GAAG,SAAS;aAC3B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,KAAK,gBAAgB,CAAC;aAChD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAErB,MAAM,WAAW,GAAG,cAAc,CAAC,SAAS,CAAC;aAC1C,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAE7D,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;YACnC,YAAY;YACZ,WAAW;SACZ,CAAC;IACJ,CAAC;CACF,CAAC,CAAC;AAEL,gFAAgF;AAChF,MAAM,CAAC,MAAM,aAAa,GAAkB,iBAAiB,EAAE,CAAC"}
|