@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
package/dist/index.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file vibe-hero MCP server entry point (T020).
3
+ *
4
+ * Bootstraps a stdio MCP server (`@modelcontextprotocol/sdk`) named `vibe-hero`
5
+ * with a `tools` capability, then registers all 10 tools from
6
+ * {@link TOOL_REGISTRY}. Each tool's handler is wrapped with the first-run setup
7
+ * gate (T021, {@link withSetupGate}) and adapted from its plain-JSON result into
8
+ * the SDK's `CallToolResult` shape ({@link toCallToolResult}).
9
+ *
10
+ * The SDK idiom (sdk 1.29.0): construct {@link McpServer}, then call
11
+ * `registerTool(name, { description, inputSchema }, cb)`. `registerTool` expects
12
+ * `inputSchema` as a *raw Zod shape* (`Record<string, ZodType>`), so we pass the
13
+ * registry's `inputSchema.shape` and the SDK hands the callback the parsed,
14
+ * validated args. Transport is {@link StdioServerTransport}.
15
+ *
16
+ * Importing this module never starts the server; only running it as the process
17
+ * entrypoint does (see the `import.meta.url` guard at the bottom), so tests can
18
+ * import {@link createServer} freely.
19
+ *
20
+ * Source of truth: specs/001-vibe-hero-mvp/contracts/mcp-tools.md, plan.md.
21
+ */
22
+ import { fileURLToPath } from "node:url";
23
+ import { argv } from "node:process";
24
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
+ import { TOOL_REGISTRY } from "./tools/placeholders.js";
27
+ import { withSetupGate } from "./tools/gate.js";
28
+ import { toCallToolResult } from "./tools/types.js";
29
+ /** Server name advertised to MCP hosts. Matches the product/skill namespace. */
30
+ export const SERVER_NAME = "vibe-hero";
31
+ /** Server version advertised to MCP hosts. */
32
+ export const SERVER_VERSION = "0.1.0";
33
+ /**
34
+ * Register one tool module on an {@link McpServer}, gating its handler and
35
+ * adapting the JSON result to a `CallToolResult`. Pulled out so both production
36
+ * and tests register tools the same way; `dirOverride` flows to the gate's
37
+ * profile lookup as a test seam.
38
+ *
39
+ * @param server - The MCP server to register onto.
40
+ * @param tool - The tool module from {@link TOOL_REGISTRY}.
41
+ * @param dirOverride - Profile-directory override (test seam) for the gate.
42
+ */
43
+ export const registerToolModule = (server, tool, dirOverride) => {
44
+ const gated = withSetupGate(tool.name, tool.handler, dirOverride);
45
+ server.registerTool(tool.name, {
46
+ description: tool.description,
47
+ // `registerTool` wants a raw Zod shape, not a wrapped object schema.
48
+ inputSchema: tool.inputSchema.shape,
49
+ }, async (args) => toCallToolResult(await gated(args)));
50
+ };
51
+ /**
52
+ * Construct the vibe-hero MCP server with all tools registered (gated). Does not
53
+ * connect a transport — call {@link main} (or wire your own transport) to start.
54
+ *
55
+ * @param dirOverride - Profile-directory override (test seam) for the gate.
56
+ * @returns A configured, not-yet-connected {@link McpServer}.
57
+ */
58
+ export const createServer = (dirOverride) => {
59
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
60
+ for (const tool of TOOL_REGISTRY) {
61
+ registerToolModule(server, tool, dirOverride);
62
+ }
63
+ return server;
64
+ };
65
+ /**
66
+ * Start the server over stdio. Connects a {@link StdioServerTransport} and
67
+ * resolves once connected; the process then stays alive serving tool calls.
68
+ */
69
+ export const main = async () => {
70
+ const server = createServer();
71
+ const transport = new StdioServerTransport();
72
+ await server.connect(transport);
73
+ };
74
+ /**
75
+ * Entrypoint guard: only auto-start when this module is the process entrypoint
76
+ * (`node .../index.js`), not when imported by tests. Compares the resolved
77
+ * module path to `argv[1]`.
78
+ */
79
+ const isEntrypoint = () => {
80
+ const entry = argv[1];
81
+ if (entry === undefined)
82
+ return false;
83
+ return fileURLToPath(import.meta.url) === entry;
84
+ };
85
+ if (isEntrypoint()) {
86
+ main().catch((err) => {
87
+ process.stderr.write(`vibe-hero: fatal error starting MCP server: ${String(err)}\n`);
88
+ process.exitCode = 1;
89
+ });
90
+ }
91
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAEpC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAsB,MAAM,kBAAkB,CAAC;AAExE,gFAAgF;AAChF,MAAM,CAAC,MAAM,WAAW,GAAG,WAAW,CAAC;AAEvC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAChC,MAAiB,EACjB,IAAmB,EACnB,WAAoB,EACd,EAAE;IACR,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAClE,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,IAAI,EACT;QACE,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,qEAAqE;QACrE,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK;KACpC,EACD,KAAK,EAAE,IAAa,EAAE,EAAE,CAAC,gBAAgB,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC,CAC7D,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,WAAoB,EAAa,EAAE;IAC9D,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,EAC9C,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;IAC5C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,YAAY,GAAG,GAAY,EAAE;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;IACtB,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC;AAClD,CAAC,CAAC;AAEF,IAAI,YAAY,EAAE,EAAE,CAAC;IACnB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+CAA+C,MAAM,CAAC,GAAG,CAAC,IAAI,CAC/D,CAAC;QACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @file Hook-payload → derived-signal extraction (FR-018, SC-008).
3
+ *
4
+ * Privacy boundary. A Claude Code `PostToolUse` hook payload carries
5
+ * `session_id`, `transcript_path`, `cwd`, `tool_name`, `tool_input`,
6
+ * `tool_output` (a.k.a. `tool_response`), and `tool_use_id` (research.md
7
+ * § Observation). The user's raw prompts, command lines, file contents, and
8
+ * tool outputs live inside `tool_input` / `tool_output`.
9
+ *
10
+ * {@link extractSignals} reads ONLY the privacy-safe fields — `tool_name`,
11
+ * derived `success`, a `timestamp`, and `tool_use_id` — and MUST NEVER copy,
12
+ * return, or persist `tool_input` or `tool_output` (or any nested raw content).
13
+ * `success` is *derived* from the shape of the output (exit code / error
14
+ * presence) WITHOUT retaining the output itself. The payload is untrusted
15
+ * `unknown` and is validated defensively.
16
+ *
17
+ * This module does NOT do topic matching: a {@link DerivedSignal} carries no
18
+ * `topicKeys`. Mapping signals → topics against `TriggerSignal` declarations
19
+ * happens later in `record_observation`. Keeping extraction topic-free makes
20
+ * the privacy contract — "no raw field ever leaves this function" — auditable
21
+ * in one place (see test/unit/privacy.test.ts).
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018, E4),
24
+ * specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
25
+ */
26
+ import { type ObservationEvent } from "../schemas/profile.js";
27
+ import { type ToolId } from "../schemas/common.js";
28
+ import { type ObservationSource } from "./source.js";
29
+ /**
30
+ * The privacy-safe projection of a single hook event. Deliberately NARROW:
31
+ * exactly the four fields the offer engine needs, and nothing derived from the
32
+ * raw input/output content.
33
+ *
34
+ * Note there is no `topicKeys` here — topic attribution is a later step
35
+ * (`record_observation`) and is intentionally out of scope for extraction.
36
+ */
37
+ export interface DerivedSignal {
38
+ /** The host tool name reported by the hook (e.g. `"Bash"`, `"Edit"`). */
39
+ readonly toolName: string;
40
+ /**
41
+ * Whether the tool call succeeded, *derived* from the output shape (exit
42
+ * code / error presence) without retaining the output. Trigger metadata
43
+ * only — never a scoring signal (FR-005).
44
+ */
45
+ readonly success: boolean;
46
+ /** ISO-8601 extraction timestamp. */
47
+ readonly timestamp: string;
48
+ /**
49
+ * The hook's `tool_use_id`, the shared id that aligns a hook event with the
50
+ * transcript's `tool_use`/`tool_result` blocks for deterministic correlation
51
+ * (FR-017). `undefined` when the payload omits it.
52
+ */
53
+ readonly toolUseId: string | undefined;
54
+ }
55
+ /**
56
+ * Extract privacy-safe {@link DerivedSignal}s from an untrusted hook payload.
57
+ *
58
+ * Reads ONLY `tool_name`, a derived `success`, a `timestamp`, and
59
+ * `tool_use_id`. NEVER reads, copies, returns, or persists `tool_input` or
60
+ * `tool_output`/`tool_response` (FR-018, SC-008). A payload missing `tool_name`
61
+ * yields no signal (nothing actionable to attribute).
62
+ *
63
+ * @param hookPayload - an untrusted `PostToolUse` hook payload.
64
+ * @param now - clock for the extraction timestamp, injectable for tests.
65
+ * Defaults to `Date.now`-backed {@link Date}.
66
+ * @returns zero or one derived signal (array for forward-compatibility with
67
+ * batched payloads).
68
+ */
69
+ export declare const extractSignals: (hookPayload: unknown, now?: () => Date) => DerivedSignal[];
70
+ /**
71
+ * {@link ObservationSource} wrapper over the Claude Code hook (FR-016).
72
+ *
73
+ * Buffers derived events as hook payloads arrive (via {@link record}) and
74
+ * drains them on {@link poll}. Topic attribution is NOT done here: because a
75
+ * {@link DerivedSignal} has no topics, this wrapper produces a *minimal*
76
+ * {@link ObservationEvent} with an empty `topicKeys` array — the downstream
77
+ * `record_observation` step matches signals to topics against `TriggerSignal`
78
+ * declarations and fills in the real keys. Keeping that out of the privacy
79
+ * boundary preserves the single-responsibility extraction contract.
80
+ *
81
+ * The buffer holds only already-derived, privacy-safe events; no raw payload is
82
+ * ever retained.
83
+ */
84
+ export declare class HookSource implements ObservationSource {
85
+ private readonly tool;
86
+ private readonly now;
87
+ readonly kind = "claude-code-hook";
88
+ private readonly buffer;
89
+ /**
90
+ * @param tool - the host tool these hook events belong to (Claude Code in
91
+ * v1). Stamped onto each derived {@link ObservationEvent}.
92
+ * @param now - clock for timestamps, injectable for deterministic tests.
93
+ */
94
+ constructor(tool?: ToolId, now?: () => Date);
95
+ /** Drain and clear the buffered derived events. */
96
+ poll(): Promise<readonly ObservationEvent[]>;
97
+ /**
98
+ * Derive privacy-safe events from one hook payload, buffer them, and return
99
+ * them. The returned events carry an empty `topicKeys` (filled downstream).
100
+ *
101
+ * @param raw - an untrusted `PostToolUse` hook payload.
102
+ */
103
+ record(raw: unknown): readonly ObservationEvent[];
104
+ /**
105
+ * Project a {@link DerivedSignal} into a minimal {@link ObservationEvent}.
106
+ * `topicKeys` is intentionally empty here (attribution is downstream). The
107
+ * `tool_use_id` becomes the correlation id when present, else a synthetic
108
+ * `hook:<timestamp>` token (FR-017 correlation needs the real id; absence is
109
+ * tolerated). Re-validated against the schema, which has no raw-content field.
110
+ */
111
+ private toEvent;
112
+ }
113
+ //# sourceMappingURL=hookEvents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hookEvents.d.ts","sourceRoot":"","sources":["../../src/observation/hookEvents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACpE,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AA2DD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,aAAa,OAAO,EACpB,MAAK,MAAM,IAAuB,KACjC,aAAa,EAyBf,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,qBAAa,UAAW,YAAW,iBAAiB;IAWhD,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAXtB,SAAgB,IAAI,sBAAsB;IAE1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0B;IAEjD;;;;OAIG;gBAEgB,IAAI,GAAE,MAAsB,EAC5B,GAAG,GAAE,MAAM,IAAuB;IAGrD,mDAAmD;IAC5C,IAAI,IAAI,OAAO,CAAC,SAAS,gBAAgB,EAAE,CAAC;IAKnD;;;;;OAKG;IACI,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,gBAAgB,EAAE;IAQxD;;;;;;OAMG;IACH,OAAO,CAAC,OAAO;CAWhB"}
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @file Hook-payload → derived-signal extraction (FR-018, SC-008).
3
+ *
4
+ * Privacy boundary. A Claude Code `PostToolUse` hook payload carries
5
+ * `session_id`, `transcript_path`, `cwd`, `tool_name`, `tool_input`,
6
+ * `tool_output` (a.k.a. `tool_response`), and `tool_use_id` (research.md
7
+ * § Observation). The user's raw prompts, command lines, file contents, and
8
+ * tool outputs live inside `tool_input` / `tool_output`.
9
+ *
10
+ * {@link extractSignals} reads ONLY the privacy-safe fields — `tool_name`,
11
+ * derived `success`, a `timestamp`, and `tool_use_id` — and MUST NEVER copy,
12
+ * return, or persist `tool_input` or `tool_output` (or any nested raw content).
13
+ * `success` is *derived* from the shape of the output (exit code / error
14
+ * presence) WITHOUT retaining the output itself. The payload is untrusted
15
+ * `unknown` and is validated defensively.
16
+ *
17
+ * This module does NOT do topic matching: a {@link DerivedSignal} carries no
18
+ * `topicKeys`. Mapping signals → topics against `TriggerSignal` declarations
19
+ * happens later in `record_observation`. Keeping extraction topic-free makes
20
+ * the privacy contract — "no raw field ever leaves this function" — auditable
21
+ * in one place (see test/unit/privacy.test.ts).
22
+ *
23
+ * Source of truth: specs/001-vibe-hero-mvp/spec.md (FR-015..018, E4),
24
+ * specs/001-vibe-hero-mvp/research.md (§ Observation & hook correlation).
25
+ */
26
+ import { ObservationEventSchema, } from "../schemas/profile.js";
27
+ /** Narrow `value` to a plain object record, else `undefined`. */
28
+ const asRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value)
29
+ ? value
30
+ : undefined;
31
+ /** Read a string field if present and non-empty, else `undefined`. */
32
+ const readString = (obj, key) => {
33
+ const v = obj[key];
34
+ return typeof v === "string" && v.length > 0 ? v : undefined;
35
+ };
36
+ /**
37
+ * Derive success from the tool output WITHOUT copying it.
38
+ *
39
+ * Heuristics, in order (any matched branch decides):
40
+ * 1. an explicit boolean `success` flag,
41
+ * 2. a numeric `exit_code` / `exitCode` (`0` ⇒ success),
42
+ * 3. presence of an `error` / `is_error` / `isError` marker (⇒ failure),
43
+ * 4. otherwise assume success (absence of an error signal).
44
+ *
45
+ * Only a boolean is ever returned; no field of `rawOutput` is retained.
46
+ *
47
+ * @param rawOutput - the untrusted `tool_output` / `tool_response` value.
48
+ */
49
+ const deriveSuccess = (rawOutput) => {
50
+ const out = asRecord(rawOutput);
51
+ if (out === undefined) {
52
+ // No structured output (or a bare scalar/array): nothing signals failure.
53
+ return true;
54
+ }
55
+ if (typeof out["success"] === "boolean") {
56
+ return out["success"];
57
+ }
58
+ const exit = out["exit_code"] ?? out["exitCode"];
59
+ if (typeof exit === "number") {
60
+ return exit === 0;
61
+ }
62
+ const isError = out["is_error"] ?? out["isError"];
63
+ if (typeof isError === "boolean") {
64
+ return !isError;
65
+ }
66
+ const err = out["error"];
67
+ if (err !== undefined && err !== null && err !== false && err !== "") {
68
+ return false;
69
+ }
70
+ return true;
71
+ };
72
+ /**
73
+ * Extract privacy-safe {@link DerivedSignal}s from an untrusted hook payload.
74
+ *
75
+ * Reads ONLY `tool_name`, a derived `success`, a `timestamp`, and
76
+ * `tool_use_id`. NEVER reads, copies, returns, or persists `tool_input` or
77
+ * `tool_output`/`tool_response` (FR-018, SC-008). A payload missing `tool_name`
78
+ * yields no signal (nothing actionable to attribute).
79
+ *
80
+ * @param hookPayload - an untrusted `PostToolUse` hook payload.
81
+ * @param now - clock for the extraction timestamp, injectable for tests.
82
+ * Defaults to `Date.now`-backed {@link Date}.
83
+ * @returns zero or one derived signal (array for forward-compatibility with
84
+ * batched payloads).
85
+ */
86
+ export const extractSignals = (hookPayload, now = () => new Date()) => {
87
+ const payload = asRecord(hookPayload);
88
+ if (payload === undefined) {
89
+ return [];
90
+ }
91
+ const toolName = readString(payload, "tool_name");
92
+ if (toolName === undefined) {
93
+ return [];
94
+ }
95
+ // `tool_output` is the documented field; some hook versions / docs use
96
+ // `tool_response`. We read it ONLY to derive a boolean and immediately drop
97
+ // the reference — its content is never copied into the result.
98
+ const success = deriveSuccess(payload["tool_output"] ?? payload["tool_response"]);
99
+ const signal = {
100
+ toolName,
101
+ success,
102
+ timestamp: now().toISOString(),
103
+ toolUseId: readString(payload, "tool_use_id"),
104
+ };
105
+ return [signal];
106
+ };
107
+ /**
108
+ * {@link ObservationSource} wrapper over the Claude Code hook (FR-016).
109
+ *
110
+ * Buffers derived events as hook payloads arrive (via {@link record}) and
111
+ * drains them on {@link poll}. Topic attribution is NOT done here: because a
112
+ * {@link DerivedSignal} has no topics, this wrapper produces a *minimal*
113
+ * {@link ObservationEvent} with an empty `topicKeys` array — the downstream
114
+ * `record_observation` step matches signals to topics against `TriggerSignal`
115
+ * declarations and fills in the real keys. Keeping that out of the privacy
116
+ * boundary preserves the single-responsibility extraction contract.
117
+ *
118
+ * The buffer holds only already-derived, privacy-safe events; no raw payload is
119
+ * ever retained.
120
+ */
121
+ export class HookSource {
122
+ tool;
123
+ now;
124
+ kind = "claude-code-hook";
125
+ buffer = [];
126
+ /**
127
+ * @param tool - the host tool these hook events belong to (Claude Code in
128
+ * v1). Stamped onto each derived {@link ObservationEvent}.
129
+ * @param now - clock for timestamps, injectable for deterministic tests.
130
+ */
131
+ constructor(tool = "claude-code", now = () => new Date()) {
132
+ this.tool = tool;
133
+ this.now = now;
134
+ }
135
+ /** Drain and clear the buffered derived events. */
136
+ poll() {
137
+ const drained = this.buffer.splice(0, this.buffer.length);
138
+ return Promise.resolve(drained);
139
+ }
140
+ /**
141
+ * Derive privacy-safe events from one hook payload, buffer them, and return
142
+ * them. The returned events carry an empty `topicKeys` (filled downstream).
143
+ *
144
+ * @param raw - an untrusted `PostToolUse` hook payload.
145
+ */
146
+ record(raw) {
147
+ const events = extractSignals(raw, this.now).map((signal) => this.toEvent(signal));
148
+ this.buffer.push(...events);
149
+ return events;
150
+ }
151
+ /**
152
+ * Project a {@link DerivedSignal} into a minimal {@link ObservationEvent}.
153
+ * `topicKeys` is intentionally empty here (attribution is downstream). The
154
+ * `tool_use_id` becomes the correlation id when present, else a synthetic
155
+ * `hook:<timestamp>` token (FR-017 correlation needs the real id; absence is
156
+ * tolerated). Re-validated against the schema, which has no raw-content field.
157
+ */
158
+ toEvent(signal) {
159
+ const topicKeys = [];
160
+ const event = {
161
+ tool: this.tool,
162
+ topicKeys,
163
+ success: signal.success,
164
+ timestamp: signal.timestamp,
165
+ correlationId: signal.toolUseId ?? `hook:${signal.timestamp}`,
166
+ };
167
+ return ObservationEventSchema.parse(event);
168
+ }
169
+ }
170
+ //# sourceMappingURL=hookEvents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hookEvents.js","sourceRoot":"","sources":["../../src/observation/hookEvents.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,215 @@
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 { type AbilityKey } from "../schemas/common.js";
29
+ import type { Config, OfferBackoff, OfferLedger } from "../schemas/profile.js";
30
+ import type { Topic } from "../schemas/content.js";
31
+ import type { OfferCandidate } from "../schemas/tools.js";
32
+ /**
33
+ * A single derived activity signal as accepted by `record_observation`. Mirrors
34
+ * the privacy-safe {@link DerivedSignal} projection — `toolName` (host tool name
35
+ * e.g. `"Bash"`), an optional `mcpTool` (an MCP tool name e.g.
36
+ * `"mcp__github__create_pr"`), an optional derived `success`, and an optional
37
+ * `toolUseId` for correlation. Trigger-only: success/ids never affect scoring.
38
+ */
39
+ export interface ObservedSignal {
40
+ readonly toolName?: string;
41
+ readonly mcpTool?: string;
42
+ readonly success?: boolean;
43
+ readonly toolUseId?: string;
44
+ }
45
+ /**
46
+ * Match derived signals against the loaded topics' trigger declarations and
47
+ * return the distinct candidate topics (deduped by {@link AbilityKey}, in
48
+ * topic-iteration order). Trigger-only (FR-015): this selects *which topic to
49
+ * offer* and never scores.
50
+ *
51
+ * Only triggers whose `tool` equals the observation's `tool` are considered, so
52
+ * a Claude Code Bash signal never trips a Codex topic. A topic with no matching
53
+ * trigger is omitted.
54
+ *
55
+ * @param topics - The loaded catalog topics (each carrying `triggerSignals`).
56
+ * @param tool - The host tool the activity belongs to.
57
+ * @param signals - The derived signals observed this turn.
58
+ * @returns The distinct offer candidates `{ key, title, reason }`.
59
+ */
60
+ export declare const matchCandidates: (topics: readonly Topic[], tool: Config["toolsLearning"][number], signals: readonly ObservedSignal[]) => OfferCandidate[];
61
+ /**
62
+ * Why an offer was suppressed, mirroring the `get_offer` contract's
63
+ * `suppressed` enum. `cadence` covers the per_session / per_topic exhaustion and
64
+ * the cross-session backoff/mute cases.
65
+ */
66
+ export type SuppressionReason = "offers_off" | "declined" | "cadence" | "no_candidate";
67
+ /** A resolved offer decision: either a chosen key, or a suppression reason. */
68
+ export type OfferDecision = {
69
+ readonly kind: "offer";
70
+ readonly key: AbilityKey;
71
+ } | {
72
+ readonly kind: "suppressed";
73
+ readonly reason: SuppressionReason;
74
+ };
75
+ /**
76
+ * The full state the pure {@link resolveOffer} decision needs. Bundles the
77
+ * relevant config flags, the current per-session ledger, the cross-session
78
+ * backoff, and the ordered candidate keys (most-relevant first) for the session.
79
+ */
80
+ export interface OfferState {
81
+ /** Master proactive-offers switch (FR-031); `false` ⇒ never offer. */
82
+ readonly proactiveOffers: boolean;
83
+ /** Configured cadence (FR-020a). */
84
+ readonly offerCadence: Config["offerCadence"];
85
+ /** The current per-session offer ledger (anti-fatigue, FR-020/020a). */
86
+ readonly ledger: OfferLedger;
87
+ /** Cross-session decline backoff + global mute (FR-020b). */
88
+ readonly backoff: OfferBackoff;
89
+ /**
90
+ * Candidate keys eligible this turn, most-relevant first. Typically the
91
+ * `offeredTopicKeys` accumulated by `record_observation`, or freshly matched
92
+ * candidates. `resolveOffer` picks the first key that survives every gate.
93
+ */
94
+ readonly candidates: readonly AbilityKey[];
95
+ }
96
+ /**
97
+ * Decide whether an end-of-work offer may surface, and for which key (FR-019
98
+ * non-interrupting timing is the caller's concern; this is the *whether/which*).
99
+ *
100
+ * Gates, in order (first failure wins):
101
+ * 1. `offerCadence === "off"` ⇒ `offers_off` (FR-020a).
102
+ * 2. `proactiveOffers === false` ⇒ `offers_off` (master switch, FR-031).
103
+ * 3. `mutedUntil` in the future ⇒ `cadence` (global mute, FR-020b).
104
+ * 4. `declinedThisSession` ⇒ `declined` (within-session suppression, FR-020).
105
+ * 5. `per_session` and an offer already surfaced this session ⇒ `cadence`
106
+ * (≤1 offer/session, FR-020a).
107
+ * 6. No candidate keys at all ⇒ `no_candidate`.
108
+ * 7. Otherwise pick the first candidate that is BOTH not already offered this
109
+ * session under `per_topic` (≤1 per distinct key/session, FR-020a) AND not
110
+ * within its cross-session `perTopicNextEligibleAt` backoff window
111
+ * (FR-020b). If none survives ⇒ `cadence`.
112
+ *
113
+ * Pure: no clock, no IO. `now` is supplied by the caller.
114
+ *
115
+ * @param state - The bundled offer state (config flags, ledger, backoff, candidates).
116
+ * @param now - The current instant, for muted/backoff comparisons.
117
+ * @returns An {@link OfferDecision}.
118
+ */
119
+ export declare const resolveOffer: (state: OfferState, now: Date) => OfferDecision;
120
+ /**
121
+ * The per-session ledger reset to a fresh `sessionId`. The cross-session backoff
122
+ * is intentionally NOT reset here — it persists across sessions until an accept
123
+ * resets it (FR-020b). Used by the tool layer when a new session begins.
124
+ */
125
+ export declare const freshLedger: (sessionId: string) => OfferLedger;
126
+ /**
127
+ * Reconcile the persisted ledger with the session being observed/queried. If the
128
+ * ledger's `sessionId` differs (or is the empty-string sentinel), the previous
129
+ * session's per-session accounting is stale, so return a {@link freshLedger}.
130
+ * Otherwise return the ledger unchanged. Pure.
131
+ *
132
+ * @param ledger - The persisted per-session ledger.
133
+ * @param sessionId - The session id of the current request.
134
+ */
135
+ export declare const ledgerForSession: (ledger: OfferLedger, sessionId: string) => OfferLedger;
136
+ /**
137
+ * Record that an offer for `key` surfaced this session: bump the per-session
138
+ * count and add the key to `offeredTopicKeys` (deduped). Pure; the tool layer
139
+ * persists the result. Called when `get_offer` actually returns an offer so the
140
+ * cadence caps are enforced on the *next* `get_offer`.
141
+ */
142
+ export declare const markOffered: (ledger: OfferLedger, key: AbilityKey) => OfferLedger;
143
+ /**
144
+ * Merge freshly-matched candidate keys into the per-session ledger's candidate
145
+ * pool (`candidateKeys`) without counting them as offered. `record_observation`
146
+ * uses this to accumulate candidates across the session as signals arrive;
147
+ * `get_offer` (which receives no signals) later resolves from this pool. New
148
+ * candidates are appended in match order, deduped, after the existing pool so
149
+ * the most-relevant-first ordering of a given turn is preserved. Does NOT touch
150
+ * `offeredTopicKeys` or `offersThisSession` — a candidate is not an offer until
151
+ * {@link markOffered}. Pure.
152
+ *
153
+ * @param ledger - The per-session ledger (already reconciled to this session).
154
+ * @param keys - The candidate keys matched this turn (most-relevant first).
155
+ */
156
+ export declare const noteCandidates: (ledger: OfferLedger, keys: readonly AbilityKey[]) => OfferLedger;
157
+ /**
158
+ * The result of applying a decline to the cross-session backoff (FR-020b): the
159
+ * updated per-session ledger (decline flag set, suppressing the rest of the
160
+ * session per FR-020) AND the updated cross-session backoff (consecutive count
161
+ * bumped, this topic's next-eligible time pushed out by exponential backoff,
162
+ * and a global mute set once `declineMuteThreshold` is reached).
163
+ */
164
+ export interface DeclineResult {
165
+ readonly ledger: OfferLedger;
166
+ readonly backoff: OfferBackoff;
167
+ }
168
+ /**
169
+ * Apply a decline for `key` (FR-020 within-session + FR-020b cross-session).
170
+ *
171
+ * Within-session (FR-020): set `declinedThisSession = true` so no further offer
172
+ * surfaces for the rest of the session.
173
+ *
174
+ * Cross-session (FR-020b):
175
+ * - increment `consecutiveDeclines`;
176
+ * - push `perTopicNextEligibleAt[key]` out by an exponential backoff:
177
+ * `backoffBaseHours * backoffFactor^(consecutiveDeclines - 1)` hours from
178
+ * `now`, so each successive decline lengthens the re-offer interval;
179
+ * - once `consecutiveDeclines >= declineMuteThreshold`, set a global
180
+ * `mutedUntil` far in the future (offers globally muted until the user
181
+ * re-enables — modeled as a long horizon derived from the backoff).
182
+ *
183
+ * Pure: `now` and config are inputs; the tool layer persists the result.
184
+ *
185
+ * @param ledger - The current per-session ledger (for this session).
186
+ * @param backoff - The current cross-session backoff.
187
+ * @param key - The declined topic key.
188
+ * @param now - The decline instant.
189
+ * @param config - Tunables (decline mute threshold + backoff base/factor).
190
+ * @returns The updated ledger + backoff.
191
+ */
192
+ export declare const applyDecline: (ledger: OfferLedger, backoff: OfferBackoff, key: AbilityKey, now: Date, config?: {
193
+ declineMuteThreshold: number;
194
+ backoffBaseHours: number;
195
+ backoffFactor: number;
196
+ }) => DeclineResult;
197
+ /**
198
+ * Apply an accept (FR-020b): reset `consecutiveDeclines` to 0 and clear the
199
+ * global `mutedUntil`. Per-topic backoff entries are left as-is (an accept on
200
+ * one topic does not retroactively un-back-off unrelated topics, but the global
201
+ * counter/mute reset means the user is engaged again). The per-session ledger is
202
+ * unchanged by an accept. Pure.
203
+ *
204
+ * @param backoff - The current cross-session backoff.
205
+ * @returns The reset backoff.
206
+ */
207
+ export declare const applyAccept: (backoff: OfferBackoff) => OfferBackoff;
208
+ /**
209
+ * Apply a defer (FR-020): a defer is treated as "ask me later" — it does NOT
210
+ * count as a decline (no backoff increment, no within-session decline flag) and
211
+ * does NOT reset the consecutive-decline counter. The state is returned
212
+ * unchanged so the next end-of-work breakpoint may re-offer normally. Pure.
213
+ */
214
+ export declare const applyDefer: (ledger: OfferLedger, backoff: OfferBackoff) => DeclineResult;
215
+ //# sourceMappingURL=offers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offers.d.ts","sourceRoot":"","sources":["../../src/observation/offers.ts"],"names":[],"mappings":"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"}