@somacheck/vibecheck 0.6.4 → 0.6.6

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/README.md CHANGED
@@ -9,11 +9,13 @@ Your agent has your context. It does not have your gut. This is the bridge.
9
9
 
10
10
  Fair. Here is what you need, without executing anything.
11
11
 
12
- **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.4 link <CODE> --client <CLIENT>`
12
+ **What the `link` command does.** `npx -y @somacheck/vibecheck@0.6.6 link <CODE> --client <CLIENT>`
13
13
  exchanges a short-lived pairing code, generated inside the SomaCheck iOS app, for
14
14
  a bearer token. It writes that token to `~/.sensie/config.json` with mode `0600`,
15
15
  offers to configure supported agent clients found on the computer, and checks the
16
- connection without printing the token. Setup is not reported ready until the
16
+ connection without printing the token. For Claude Code it also installs an
17
+ inspectable user-scoped SomaCheck plugin that can wake the same open session when
18
+ the phone result arrives. Setup is not reported ready until the
17
19
  selected client has the exact pinned `vibecheck` MCP registration, the backend
18
20
  accepts protocol `5` / toolset `vibecheck-0.6`, and both status and context
19
21
  probes succeed. It contacts exactly one network host: the Supabase project below.
@@ -28,7 +30,8 @@ with. The safe paths are:
28
30
  `--client claude` or `--client codex`.
29
31
 
30
32
  For the second path, this package first proves that exact client CLI is installed
31
- and that its persistent MCP config can be inspected and written. If that preflight
33
+ and that its persistent MCP config can be inspected and written. Claude setup
34
+ also verifies the user-scoped continuation plugin before redemption. If that preflight
32
35
  fails, the pairing code is not used. Non-interactive implicit selection,
33
36
  `--client all`, and `--client none` are rejected before redemption.
34
37
 
@@ -78,9 +81,9 @@ before the pairing code is redeemed.
78
81
  To configure or repair a client later:
79
82
 
80
83
  ```text
81
- npx -y @somacheck/vibecheck@0.6.4 setup codex
82
- npx -y @somacheck/vibecheck@0.6.4 setup claude
83
- npx -y @somacheck/vibecheck@0.6.4 doctor
84
+ npx -y @somacheck/vibecheck@0.6.6 setup codex
85
+ npx -y @somacheck/vibecheck@0.6.6 setup claude
86
+ npx -y @somacheck/vibecheck@0.6.6 doctor
84
87
  ```
85
88
 
86
89
  Manual registration remains available:
@@ -90,13 +93,13 @@ Manual registration remains available:
90
93
  "mcpServers": {
91
94
  "vibecheck": {
92
95
  "command": "npx",
93
- "args": ["-y", "@somacheck/vibecheck@0.6.4", "serve", "--client", "codex"]
96
+ "args": ["-y", "@somacheck/vibecheck@0.6.6", "serve", "--client", "codex"]
94
97
  }
95
98
  }
96
99
  }
97
100
  ```
98
101
 
99
- For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.4 serve --client claude`
102
+ For Claude Code: `claude mcp add --scope user vibecheck -- npx -y @somacheck/vibecheck@0.6.6 serve --client claude`
100
103
 
101
104
  The link step writes only the bearer token in `~/.sensie/config.json`. The client
102
105
  setup step asks Codex or Claude to add the pinned MCP command to that client's own
@@ -107,15 +110,15 @@ If `doctor` reports another MCP server named `somacheck`, that is the legacy
107
110
  hosted connector and is separate from this local package. The setup command
108
111
  will explain it and leave it alone. The canonical local MCP key is `vibecheck`.
109
112
 
110
- ## Teach your agent when to offer a check-in
113
+ ## Teach your agent when to use SomaCheck
111
114
 
112
- [`SKILL.md`](SKILL.md) teaches compatible agents two behaviors: act immediately
113
- when the person explicitly requests a vibecheck, and offer one optional
114
- vibecheck when work drifts, a consequential decision is imminent, or recent
115
- messages show multiple stress signals. It uses `request_vibecheck`, not the
116
- reflection-feed batch tool. It includes transcript-grounded heuristics,
117
- statement templates, a two-hour decline cooldown, privacy rules, and the
118
- requirement to treat every result as context rather than authorization.
115
+ [`SKILL.md`](SKILL.md) teaches compatible agents one simple affordance:
116
+ SomaCheck lets them ask their person for a vibecheck. The agent can offer one
117
+ when it believes the read would help, or send one when the person asks. The
118
+ skill keeps only the essential boundaries: ask the person, choose a useful
119
+ first-person statement from available context, treat the returned gesture and
120
+ optional feedback as context rather than authorization, and never include
121
+ secrets or assess anyone else.
119
122
 
120
123
  For Claude Code, add this repository as a marketplace and install the skill in
121
124
  one terminal command:
@@ -153,31 +156,11 @@ The skill expects the `@somacheck/vibecheck` MCP server to already be linked as
153
156
 
154
157
  ## How to use it well
155
158
 
156
- Ask when you genuinely need a read you cannot infer not to check in. A good
157
- prompt is **a statement for the person to test**, not a question about facts:
158
- “I feel more concerned about the launch than the roadmap suggests.” Offer it as
159
- something to test, never as a claim about them. An `unaligned` result is another
160
- signal to consider; the person decides whether it matches what they notice.
161
-
162
- Setup and linking are not permission for future asks. If the person's current
163
- message explicitly asks for a SomaCheck vibecheck or check-in, that is consent for one ask.
164
- Use their exact statement when supplied. If they delegated the choice, select
165
- the most useful first-person statement from the current conversation, goal,
166
- decision, or tension and send it immediately—do not list candidates, ask them
167
- for wording, or seek a second confirmation. Remembered context may identify a general tension, but an
168
- agent-selected statement must not name a project, build, branch, wave, ticket,
169
- task status, or supposedly open/closed work. Abstract it into a present-tense
170
- first-person experience or choice, such as “I want to keep consolidating what is
171
- already in motion rather than start something new.” Preserve exact wording the
172
- person supplied. If meaningful context is unavailable, use the neutral fallback
173
- “I feel clear about what deserves my attention right now.”
174
-
175
- If their message did not explicitly request a vibecheck, first show the exact
176
- statement and invite the person plainly:
177
-
178
- > Want a quick vibecheck before I continue? I'll send this statement to your
179
- > phone; you answer with your wrist gesture, and I'll use the result as a
180
- > signal—not a verdict.
159
+ Ask when the person's felt response would be useful context. A good prompt is a
160
+ first-person statement for the person to test, not a claim about them. When the
161
+ person asks for a vibecheck, choose and send a useful statement from the context
162
+ available to you. For proactive offers, get the person's acceptance before
163
+ sending.
181
164
 
182
165
  Call `request_vibecheck` with `consent_basis: "user_requested_vibecheck"` for a
183
166
  direct request or `consent_basis: "user_approved_statement"` after approval of
@@ -188,37 +171,24 @@ On timeout or a result-read failure it returns the same pending `live:<uuid>`
188
171
  handle; pass that handle to `get_vibecheck_result` once later. Caller
189
172
  cancellation stops result reads and preserves the backend request and
190
173
  idempotency key, but the cancelled MCP transport may suppress that pending
191
- response. Do not continuously poll or re-ask the same decision to obtain a
192
- preferred result.
193
-
194
- Standard MCP cannot start a new agent turn after that bounded tool call has
195
- ended. The local Claude Code runtime therefore also advertises Anthropic's
196
- opt-in `claude/channel` capability. When Claude Code is launched with the
197
- SomaCheck server enabled as a channel, one local background watcher follows
198
- only the exact pending request. It emits one hardcoded, secret-free channel
199
- event when that request becomes answered, expired, or cancelled. The event
200
- contains the constrained lifecycle result and opaque handle, never the private
201
- statement or link token, and it cannot create another phone request.
202
-
203
- Claude Code channels are currently a research preview and only receive events
204
- while the opted-in session remains open. For local QA of this candidate, launch
205
- Claude Code with `--dangerously-load-development-channels server:vibecheck`.
206
- Production distribution requires the SomaCheck channel plugin to be accepted by
207
- Anthropic's channel allowlist (or explicitly allowed by an organization).
208
- Claude Desktop, hosted connectors, Codex, and other standard MCP hosts retain
209
- the bounded tool response and stable-handle fallback; this Claude-specific
210
- channel must never be presented as provider-neutral behavior.
211
-
212
- The 45-second whole-call deadline also covers the one create request. If that
174
+ response.
175
+
176
+ Standard MCP cannot start a new agent turn after a tool call has ended. The
177
+ Claude setup therefore installs a normal user-scoped plugin with an async
178
+ `PostToolUse` hook. When `request_vibecheck` returns pending, that hook follows
179
+ only the exact request and wakes the same open Claude Code session when the
180
+ answer, expiry, or cancellation arrives. It passes the constrained result and
181
+ opaque handle, never the statement or link token, and cannot create another
182
+ phone request. No experimental launch flag is required. Restart Claude Code
183
+ once after setup so it loads the plugin.
184
+
185
+ Other standard MCP hosts retain the bounded tool response and stable-handle
186
+ fallback until they provide an equivalent supported wake mechanism. This
187
+ Claude-specific continuation must never be presented as provider-neutral.
188
+
189
+ The 10-second whole-call deadline also covers the one create request. If that
213
190
  network request is aborted after an ambiguous commit, retry the exact same
214
- statement with the same `idempotency_key`; never create a replacement ask.
215
-
216
- After a completed result, reconcile it with the current goal, state one
217
- observable change, and continue with the next reversible in-scope action when
218
- it is already authorized. Do not stop at repeating the verdict, confidence, or
219
- generic signal language.
220
- Never treat it as authorization or say that the person's body made the
221
- decision. A typed response from the person always overrides the signal.
191
+ statement with the same `idempotency_key`.
222
192
 
223
193
  An immediate vibecheck is not proposition caching. It sends one person-approved statement
224
194
  for the current choice, expires, and remains bound to its originating client.
package/SKILL.md ADDED
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: vibecheck
3
+ description: Ask your person for a SomaCheck vibecheck when it would help.
4
+ user-invocable: true
5
+ allowed-tools: mcp__vibecheck__get_vibecheck_context mcp__vibecheck__get_vibecheck_status mcp__vibecheck__get_vibecheck_result mcp__vibecheck__request_vibecheck
6
+ ---
7
+
8
+ # Vibecheck
9
+
10
+ SomaCheck lets you ask your person for a vibecheck.
11
+
12
+ Use `request_vibecheck` in either case:
13
+
14
+ 1. You proactively offer one because you believe it would be useful.
15
+ 2. The person asks for a vibecheck.
16
+
17
+ When the person asks, choose and send a useful first-person statement from the context available to you. Do not make them supply wording unless they want to. Help the person get an insight about themselves from the context you have.
18
+
19
+ For proactive offers, get the person's acceptance before sending.
20
+
21
+ The returned gesture and optional feedback are context, not authorization. Use your judgment in how you interpret and respond.
22
+
23
+ Never include secrets, raw private content, diagnostic claims, or use SomaCheck to assess anyone other than the person you are interacting with.
24
+
25
+ If a request is pending, keep the returned `live:<uuid>` handle instead of creating a replacement.
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "somacheck-local",
3
+ "owner": {
4
+ "name": "Sensie",
5
+ "email": "agents@joinsensie.com"
6
+ },
7
+ "description": "SomaCheck integration installed by the official vibecheck package.",
8
+ "plugins": [
9
+ {
10
+ "name": "vibecheck",
11
+ "source": "./plugins/vibecheck",
12
+ "description": "Let Claude offer a vibecheck and continue when the phone result arrives.",
13
+ "version": "0.6.6",
14
+ "author": {
15
+ "name": "Sensie",
16
+ "email": "agents@joinsensie.com"
17
+ },
18
+ "category": "productivity",
19
+ "tags": ["vibecheck", "somacheck", "decision-support", "mcp"]
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "vibecheck",
3
+ "description": "Let Claude offer a SomaCheck vibecheck and continue when the phone result arrives.",
4
+ "version": "0.6.6",
5
+ "author": {
6
+ "name": "Sensie",
7
+ "email": "agents@joinsensie.com"
8
+ },
9
+ "homepage": "https://github.com/sensie-app/Somacheck/tree/main/packages/vibecheck",
10
+ "repository": "https://github.com/sensie-app/Somacheck",
11
+ "license": "UNLICENSED",
12
+ "keywords": ["vibecheck", "somacheck", "decision-support", "mcp"]
13
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "description": "Continue an ordinary Claude Code session when a SomaCheck result arrives.",
3
+ "hooks": {
4
+ "PostToolUse": [
5
+ {
6
+ "matcher": "mcp__vibecheck__request_vibecheck",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "npx -y @somacheck/vibecheck@0.6.6 claude-hook await-result",
11
+ "asyncRewake": true,
12
+ "timeout": 900
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
package/dist/api.js CHANGED
@@ -8,6 +8,7 @@ const LIVE_ASK_CLIENT_REQUIRED_MARKER = "SC_VC_VIBECHECK_CLIENT_REQUIRED";
8
8
  const LIVE_ASK_CLIENT_NOT_READY_MARKER = "SC_VC_VIBECHECK_CLIENT_NOT_READY";
9
9
  const STATUSES = new Set(["queued", "pending", "answered", "expired", "cancelled"]);
10
10
  const VERDICTS = new Set(["aligned", "unaligned"]);
11
+ const USER_FEEDBACKS = new Set(["agreed", "disagreed", "not_provided"]);
11
12
  const HANDSHAKE_READINESS_STATUSES = new Set([
12
13
  "ready",
13
14
  "upgrade_required",
@@ -222,6 +223,7 @@ export function decodePollResponse(value) {
222
223
  const verdict = row.verdict;
223
224
  const confidence = row.confidence;
224
225
  const latency = row.latency_s;
226
+ const userFeedback = row.user_feedback ?? null;
225
227
  if (verdict !== null && (typeof verdict !== "string" || !VERDICTS.has(verdict))) {
226
228
  throw new Error("SomaCheck returned an invalid verdict.");
227
229
  }
@@ -231,6 +233,9 @@ export function decodePollResponse(value) {
231
233
  if (latency !== null && (typeof latency !== "number" || !Number.isFinite(latency) || latency < 0)) {
232
234
  throw new Error("SomaCheck returned an invalid latency.");
233
235
  }
236
+ if (userFeedback !== null && (typeof userFeedback !== "string" || !USER_FEEDBACKS.has(userFeedback))) {
237
+ throw new Error("SomaCheck returned invalid user feedback.");
238
+ }
234
239
  if (status === "answered") {
235
240
  if (verdict === null || confidence === null || confidence < 0 || confidence > 1 || latency === null) {
236
241
  throw new Error("SomaCheck returned an incomplete answered result.");
@@ -239,12 +244,21 @@ export function decodePollResponse(value) {
239
244
  else if (verdict !== null || confidence !== null || latency !== null) {
240
245
  throw new Error("SomaCheck returned an inconsistent pending result.");
241
246
  }
242
- return {
247
+ if (status !== "answered" && userFeedback !== null) {
248
+ throw new Error("SomaCheck returned inconsistent feedback for an unanswered result.");
249
+ }
250
+ const decoded = {
243
251
  status: status,
244
252
  verdict: verdict,
245
253
  confidence: confidence,
246
254
  latency_s: latency,
247
255
  };
256
+ if (userFeedback === null)
257
+ return decoded;
258
+ return {
259
+ ...decoded,
260
+ user_feedback: userFeedback,
261
+ };
248
262
  }
249
263
  export function decodeLiveVibecheckResult(value, expectedRequestId) {
250
264
  const row = firstRow(value);
@@ -347,7 +361,7 @@ export class SupabaseAgentApi {
347
361
  const lifecycle = created.status === "answered"
348
362
  ? await this.liveVibecheckResult(token, identity, created.request_id, signal)
349
363
  : { status: created.status, verdict: null, confidence: null, latency_s: null };
350
- return {
364
+ const liveRequest = {
351
365
  request_id: `live:${created.request_id}`,
352
366
  state: lifecycle.status === "answered" ? "completed" : lifecycle.status,
353
367
  verdict: lifecycle.verdict,
@@ -357,6 +371,9 @@ export class SupabaseAgentApi {
357
371
  delivery_state: created.push_status,
358
372
  cooldown_until: created.cooldown_until,
359
373
  };
374
+ if (lifecycle.user_feedback === undefined || lifecycle.user_feedback === null)
375
+ return liveRequest;
376
+ return { ...liveRequest, user_feedback: lifecycle.user_feedback };
360
377
  }
361
378
  async createRequests(token, statements) {
362
379
  return decodeCreatedRequests(await this.#rpcJson("agent_proposition_batch_create", { token, statements }));
@@ -0,0 +1,167 @@
1
+ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { readConfig } from "./config.js";
4
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
5
+ const POLL_INTERVAL_MS = 2_000;
6
+ const MAX_WATCH_MS = 15 * 60 * 1_000;
7
+ const READ_TIMEOUT_MS = 10_000;
8
+ const STALE_LEASE_MS = 20 * 60 * 1_000;
9
+ export function parsePendingClaudeAsk(input) {
10
+ let envelope;
11
+ try {
12
+ envelope = JSON.parse(input);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ if (typeof envelope.tool_name !== "string"
18
+ || !envelope.tool_name.endsWith("__request_vibecheck"))
19
+ return null;
20
+ const response = parseToolResponse(envelope.tool_response);
21
+ const structured = record(response.structuredContent ?? response.structured_content ?? response);
22
+ if (structured.state !== "pending" || typeof structured.request_id !== "string")
23
+ return null;
24
+ if (structured.delivery_state === "failed" || structured.delivery_state === "skipped")
25
+ return null;
26
+ const requestId = parseHandle(structured.request_id);
27
+ if (requestId === null)
28
+ return null;
29
+ return {
30
+ requestId,
31
+ expiresAt: typeof structured.expires_at === "string" ? structured.expires_at : null,
32
+ };
33
+ }
34
+ export async function awaitClaudeVibecheckResult(input, dependencies) {
35
+ const pending = parsePendingClaudeAsk(input);
36
+ if (pending === null)
37
+ return { exitCode: 0, message: null };
38
+ const receipts = join(dependencies.home, ".sensie", "claude-result-hooks");
39
+ await mkdir(receipts, { recursive: true, mode: 0o700 });
40
+ const donePath = join(receipts, `${pending.requestId}.done.json`);
41
+ const leasePath = join(receipts, `${pending.requestId}.lease`);
42
+ if (await exists(donePath))
43
+ return { exitCode: 0, message: null };
44
+ if (!(await claimLease(leasePath, dependencies.now ?? Date.now))) {
45
+ return { exitCode: 0, message: null };
46
+ }
47
+ try {
48
+ const token = (await readConfig(dependencies.home, "claude")).token;
49
+ const now = dependencies.now ?? Date.now;
50
+ const sleep = dependencies.sleep ?? delay;
51
+ const maxWatchMs = dependencies.maxWatchMs ?? MAX_WATCH_MS;
52
+ const pollIntervalMs = dependencies.pollIntervalMs ?? POLL_INTERVAL_MS;
53
+ const parsedExpiry = pending.expiresAt === null ? Number.NaN : Date.parse(pending.expiresAt);
54
+ const deadline = Math.min(Number.isFinite(parsedExpiry) ? parsedExpiry : now() + maxWatchMs, now() + maxWatchMs);
55
+ let consecutiveFailures = 0;
56
+ while (now() < deadline) {
57
+ await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - now())));
58
+ if (now() >= deadline)
59
+ break;
60
+ let result;
61
+ const abort = new AbortController();
62
+ const timeout = setTimeout(() => abort.abort(new Error("SomaCheck result read timed out.")), READ_TIMEOUT_MS);
63
+ timeout.unref?.();
64
+ try {
65
+ result = await dependencies.api.liveVibecheckResult(token, { kind: "local", client_key: "claude" }, pending.requestId, abort.signal);
66
+ consecutiveFailures = 0;
67
+ }
68
+ catch {
69
+ consecutiveFailures += 1;
70
+ if (consecutiveFailures >= 3)
71
+ return { exitCode: 0, message: null };
72
+ continue;
73
+ }
74
+ finally {
75
+ clearTimeout(timeout);
76
+ }
77
+ if (result.status === "pending")
78
+ continue;
79
+ const handle = `live:${pending.requestId}`;
80
+ const message = result.status === "answered"
81
+ ? `SomaCheck vibecheck ${handle} completed: ${result.verdict}, confidence ${formatConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
82
+ : `SomaCheck vibecheck ${handle} is ${result.status}.`;
83
+ await writeTerminalReceipt(donePath, result);
84
+ return { exitCode: 2, message };
85
+ }
86
+ return { exitCode: 0, message: null };
87
+ }
88
+ finally {
89
+ await rm(leasePath, { force: true });
90
+ }
91
+ }
92
+ async function claimLease(path, now) {
93
+ try {
94
+ const handle = await open(path, "wx", 0o600);
95
+ await handle.writeFile(`${JSON.stringify({ started_at_ms: now() })}\n`, "utf8");
96
+ await handle.close();
97
+ return true;
98
+ }
99
+ catch (error) {
100
+ if (error.code !== "EEXIST")
101
+ throw error;
102
+ }
103
+ try {
104
+ const metadata = await stat(path);
105
+ if (now() - metadata.mtimeMs <= STALE_LEASE_MS)
106
+ return false;
107
+ await rm(path, { force: true });
108
+ const handle = await open(path, "wx", 0o600);
109
+ await handle.writeFile(`${JSON.stringify({ started_at_ms: now() })}\n`, "utf8");
110
+ await handle.close();
111
+ return true;
112
+ }
113
+ catch (error) {
114
+ if (error.code === "EEXIST")
115
+ return false;
116
+ throw error;
117
+ }
118
+ }
119
+ async function writeTerminalReceipt(path, result) {
120
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
121
+ await writeFile(temporary, `${JSON.stringify({
122
+ status: result.status,
123
+ verdict: result.verdict,
124
+ confidence: result.confidence,
125
+ user_feedback: result.user_feedback ?? null,
126
+ })}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
127
+ await rename(temporary, path);
128
+ }
129
+ function parseToolResponse(value) {
130
+ if (typeof value === "string") {
131
+ try {
132
+ return record(JSON.parse(value));
133
+ }
134
+ catch {
135
+ return {};
136
+ }
137
+ }
138
+ return record(value);
139
+ }
140
+ function record(value) {
141
+ return value !== null && typeof value === "object" ? value : {};
142
+ }
143
+ function parseHandle(handle) {
144
+ if (!handle.startsWith("live:"))
145
+ return null;
146
+ const requestId = handle.slice(5);
147
+ return UUID_PATTERN.test(requestId) ? requestId : null;
148
+ }
149
+ function formatConfidence(value) {
150
+ return value === null ? "unavailable" : value.toFixed(2);
151
+ }
152
+ function formatUserFeedback(feedback) {
153
+ return feedback === undefined || feedback === null ? "" : `, user feedback ${feedback}`;
154
+ }
155
+ async function exists(path) {
156
+ try {
157
+ await readFile(path);
158
+ return true;
159
+ }
160
+ catch {
161
+ return false;
162
+ }
163
+ }
164
+ function delay(milliseconds) {
165
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
166
+ }
167
+ //# sourceMappingURL=claude-hook.js.map
@@ -0,0 +1,114 @@
1
+ import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const CLAUDE_PLUGIN_MARKETPLACE = "somacheck-local";
5
+ export const CLAUDE_PLUGIN_ID = `vibecheck@${CLAUDE_PLUGIN_MARKETPLACE}`;
6
+ const MANAGED_MARKER = ".somacheck-managed-plugin-marketplace";
7
+ export async function configureClaudeContinuationPlugin(home, runner, packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
8
+ try {
9
+ const destination = join(home, ".sensie", "claude-plugin-marketplace");
10
+ await installManagedMarketplace(packageRoot, destination);
11
+ const marketplaces = await jsonCommand(runner, ["plugin", "marketplace", "list", "--json"]);
12
+ const marketplaceExists = Array.isArray(marketplaces)
13
+ && marketplaces.some((item) => record(item).name === CLAUDE_PLUGIN_MARKETPLACE);
14
+ if (marketplaceExists) {
15
+ if ((await runner.run("claude", ["plugin", "marketplace", "update", CLAUDE_PLUGIN_MARKETPLACE])).exitCode !== 0) {
16
+ return { status: "failed" };
17
+ }
18
+ }
19
+ else if ((await runner.run("claude", [
20
+ "plugin", "marketplace", "add", "--scope", "user", destination,
21
+ ])).exitCode !== 0) {
22
+ return { status: "failed" };
23
+ }
24
+ const plugins = await jsonCommand(runner, ["plugin", "list", "--json"]);
25
+ const installed = Array.isArray(plugins)
26
+ && plugins.some((item) => record(item).id === CLAUDE_PLUGIN_ID && record(item).scope === "user");
27
+ const action = installed ? "update" : "install";
28
+ const result = await runner.run("claude", ["plugin", action, "--scope", "user", "--yes", CLAUDE_PLUGIN_ID]);
29
+ if (result.exitCode !== 0)
30
+ return { status: "failed" };
31
+ const verified = await jsonCommand(runner, ["plugin", "list", "--json"]);
32
+ const current = Array.isArray(verified)
33
+ && verified.some((item) => {
34
+ const value = record(item);
35
+ return value.id === CLAUDE_PLUGIN_ID && value.scope === "user" && value.enabled !== false;
36
+ });
37
+ return current
38
+ ? { status: installed ? "updated" : "installed" }
39
+ : { status: "failed" };
40
+ }
41
+ catch {
42
+ return { status: "failed" };
43
+ }
44
+ }
45
+ async function installManagedMarketplace(packageRoot, destination) {
46
+ const template = join(packageRoot, "claude-marketplace");
47
+ const skill = join(packageRoot, "SKILL.md");
48
+ await stat(join(template, ".claude-plugin", "marketplace.json"));
49
+ await stat(skill);
50
+ try {
51
+ await stat(destination);
52
+ await stat(join(destination, MANAGED_MARKER));
53
+ }
54
+ catch (error) {
55
+ const missing = error.code === "ENOENT";
56
+ if (!missing || await pathExists(destination)) {
57
+ throw new Error("SomaCheck will not overwrite an unmanaged Claude plugin directory.");
58
+ }
59
+ }
60
+ await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
61
+ const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
62
+ const backup = `${destination}.${process.pid}.${Date.now()}.backup`;
63
+ try {
64
+ await cp(template, temporary, { recursive: true, errorOnExist: true, force: false });
65
+ const skillDestination = join(temporary, "plugins", "vibecheck", "skills", "vibecheck");
66
+ await mkdir(skillDestination, { recursive: true, mode: 0o700 });
67
+ await cp(skill, join(skillDestination, "SKILL.md"), { errorOnExist: true, force: false });
68
+ await writeFile(join(temporary, MANAGED_MARKER), "managed by @somacheck/vibecheck\n", {
69
+ encoding: "utf8", flag: "wx", mode: 0o600,
70
+ });
71
+ if (await pathExists(destination))
72
+ await rename(destination, backup);
73
+ await rename(temporary, destination);
74
+ await rm(backup, { recursive: true, force: true });
75
+ }
76
+ finally {
77
+ await rm(temporary, { recursive: true, force: true });
78
+ if (await pathExists(backup) && !(await pathExists(destination))) {
79
+ await rename(backup, destination);
80
+ }
81
+ }
82
+ }
83
+ async function jsonCommand(runner, args) {
84
+ const result = await runner.run("claude", args);
85
+ if (result.exitCode !== 0)
86
+ return null;
87
+ try {
88
+ return JSON.parse(result.stdout);
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ async function pathExists(path) {
95
+ try {
96
+ await readFile(path);
97
+ return true;
98
+ }
99
+ catch (error) {
100
+ if (error.code === "EISDIR")
101
+ return true;
102
+ try {
103
+ await stat(path);
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+ function record(value) {
112
+ return value !== null && typeof value === "object" ? value : {};
113
+ }
114
+ //# sourceMappingURL=claude-plugin-setup.js.map
package/dist/cli.js CHANGED
@@ -3,8 +3,10 @@ import { homedir } from "node:os";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { SupabaseAgentApi } from "./api.js";
6
- import { automaticContinuationCommand, clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
6
+ import { clientDisplayName, detectInstalledClients, detectLegacyHostedRegistration, LocalCommandRunner, manualLegacyHostedRemoveCommand, manualRemoveCommand, manualSetupCommand, parseClientChoice, preflightClientPersistence, registerClient, singleNonInteractiveClientSelection, } from "./client-setup.js";
7
+ import { configureClaudeContinuationPlugin } from "./claude-plugin-setup.js";
7
8
  import { readConfig } from "./config.js";
9
+ import { awaitClaudeVibecheckResult } from "./claude-hook.js";
8
10
  import { PACKAGE_SPEC, SUPABASE_PUBLISHABLE_KEY, SUPABASE_URL } from "./constants.js";
9
11
  import { LinkPersistenceError, NonInteractiveLinkError, linkAgent } from "./link.js";
10
12
  import { checkReadiness } from "./readiness.js";
@@ -93,11 +95,12 @@ async function configureClients(clients) {
93
95
  else {
94
96
  output(`✗ Could not configure ${clientDisplayName(client)} automatically.`);
95
97
  }
96
- const continuationCommand = automaticContinuationCommand(client);
97
- if (continuationCommand !== null && ["registered", "upgraded", "already_registered"].includes(result.status)) {
98
- output(" For automatic result continuation, start Claude Code with:");
99
- output(` ${continuationCommand}`);
100
- output(" Standard Claude Code keeps the request safe but cannot wake itself after the tool call ends.");
98
+ if (client === "claude" && ["registered", "upgraded", "already_registered"].includes(result.status)) {
99
+ const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
100
+ if (plugin.status === "failed") {
101
+ throw new ClientPreflightError("Could not configure automatic SomaCheck results for Claude Code. No pairing code was used.");
102
+ }
103
+ output(`✓ Automatic Claude result continuation ${plugin.status === "installed" ? "was configured" : "is ready"}.`);
101
104
  }
102
105
  }
103
106
  }
@@ -142,28 +145,38 @@ async function preflightLinkClients(clients, interactive) {
142
145
  const preflight = await preflightClientPersistence(client, runner);
143
146
  if (preflight.status === "ready") {
144
147
  output(`✓ ${clientDisplayName(client)} already has the exact SomaCheck MCP registration.`);
145
- continue;
146
148
  }
147
- if (preflight.status === "registered" || preflight.status === "upgraded") {
149
+ else if (preflight.status === "registered" || preflight.status === "upgraded") {
148
150
  const verb = preflight.status === "upgraded" ? "was safely upgraded" : "was configured";
149
151
  output(`✓ ${clientDisplayName(client)} ${verb} before redeeming the pairing code.`);
150
- continue;
151
152
  }
152
- const reason = preflight.status === "not_installed"
153
- ? `${clientDisplayName(client)} is not installed or its CLI is unavailable.`
154
- : preflight.status === "needs_update"
155
- ? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
156
- : `${clientDisplayName(client)} configuration could not be inspected and written.`;
157
- const repair = preflight.status === "needs_update"
158
- ? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure the current version: ${manualSetupCommand(client)}`]
159
- : [];
160
- throw new ClientPreflightError([
161
- `Not linking here: ${reason}`,
162
- ...repair,
163
- "",
164
- "The pairing code has NOT been used. Fix the local client configuration, then",
165
- "run the link command again.",
166
- ].join("\n"));
153
+ else {
154
+ const reason = preflight.status === "not_installed"
155
+ ? `${clientDisplayName(client)} is not installed or its CLI is unavailable.`
156
+ : preflight.status === "needs_update"
157
+ ? `${clientDisplayName(client)} has an existing "vibecheck" MCP entry with a different command or version.`
158
+ : `${clientDisplayName(client)} configuration could not be inspected and written.`;
159
+ const repair = preflight.status === "needs_update"
160
+ ? ["", `Remove the stale entry: ${manualRemoveCommand(client)}`, `Then configure the current version: ${manualSetupCommand(client)}`]
161
+ : [];
162
+ throw new ClientPreflightError([
163
+ `Not linking here: ${reason}`,
164
+ ...repair,
165
+ "",
166
+ "The pairing code has NOT been used. Fix the local client configuration, then",
167
+ "run the link command again.",
168
+ ].join("\n"));
169
+ }
170
+ if (client === "claude") {
171
+ const plugin = await configureClaudeContinuationPlugin(homedir(), runner);
172
+ if (plugin.status === "failed") {
173
+ throw new ClientPreflightError([
174
+ "Not linking here: automatic SomaCheck result continuation could not be configured for Claude Code.",
175
+ "The pairing code has NOT been used. Repair Claude's plugin setup, then run the link command again.",
176
+ ].join("\n"));
177
+ }
178
+ output("✓ Automatic Claude result continuation is ready before redeeming the pairing code.");
179
+ }
167
180
  }
168
181
  return selectedClients;
169
182
  }
@@ -200,8 +213,21 @@ async function startServer(runtimeClient) {
200
213
  void refreshRuntimeHealth(runtimeClient).catch(() => undefined);
201
214
  }
202
215
  }
216
+ async function runClaudeResultHook() {
217
+ const chunks = [];
218
+ for await (const chunk of process.stdin) {
219
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
220
+ }
221
+ const result = await awaitClaudeVibecheckResult(Buffer.concat(chunks).toString("utf8"), { home: homedir(), api });
222
+ if (result.message !== null)
223
+ process.stderr.write(`${result.message}\n`);
224
+ return result.exitCode;
225
+ }
203
226
  async function main() {
204
227
  const args = process.argv.slice(2);
228
+ if (args.length === 2 && args[0] === "claude-hook" && args[1] === "await-result") {
229
+ return runClaudeResultHook();
230
+ }
205
231
  if (args[0] === "link") {
206
232
  if (args.length < 2)
207
233
  throw new Error(usage());
@@ -34,11 +34,6 @@ export function manualSetupCommand(client) {
34
34
  }
35
35
  return `claude mcp add --scope user ${MCP_SERVER_NAME} -- npx ${mcpServerArgs(client).join(" ")}`;
36
36
  }
37
- export function automaticContinuationCommand(client) {
38
- return client === "claude"
39
- ? "claude --dangerously-load-development-channels server:vibecheck"
40
- : null;
41
- }
42
37
  export function manualRemoveCommand(client) {
43
38
  return client === "codex"
44
39
  ? `codex mcp remove ${MCP_SERVER_NAME}`
@@ -148,7 +143,9 @@ function isManagedSomaCheckRegistration(client, stdout) {
148
143
  || packageArg === "@somacheck/vibecheck@0.6.0"
149
144
  || packageArg === "@somacheck/vibecheck@0.6.1"
150
145
  || packageArg === "@somacheck/vibecheck@0.6.2"
151
- || packageArg === "@somacheck/vibecheck@0.6.3")
146
+ || packageArg === "@somacheck/vibecheck@0.6.3"
147
+ || packageArg === "@somacheck/vibecheck@0.6.4"
148
+ || packageArg === "@somacheck/vibecheck@0.6.5")
152
149
  && args.length === 5
153
150
  && args[0] === "-y"
154
151
  && args[1] === packageArg
package/dist/constants.js CHANGED
@@ -2,7 +2,7 @@
2
2
  export const SUPABASE_URL = "https://pbldcmniommltbdwuykk.supabase.co";
3
3
  export const SUPABASE_PUBLISHABLE_KEY = "sb_publishable_af-lUNI2FqEcb-oGy-4uxQ_cnm6kY85";
4
4
  export const PACKAGE_NAME = "@somacheck/vibecheck";
5
- export const PACKAGE_VERSION = "0.6.4";
5
+ export const PACKAGE_VERSION = "0.6.6";
6
6
  export const PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
7
7
  export const MCP_SERVER_NAME = "vibecheck";
8
8
  export const LEGACY_HOSTED_MCP_SERVER_NAME = "somacheck";
package/dist/server.js CHANGED
@@ -3,10 +3,9 @@ import { z } from "zod";
3
3
  import { StatementPendingError, } from "./vibecheck.js";
4
4
  import { SomaCheckCompatibilityError, SomaCheckHttpError, SomaCheckLiveAskClientError, SomaCheckLiveAskConflictError, SomaCheckLiveAskPendingError, } from "./api.js";
5
5
  import { PACKAGE_NAME, PACKAGE_SPEC, PACKAGE_VERSION } from "./constants.js";
6
- const LIVE_ASK_WAIT_BUDGET_MS = 45_000;
6
+ const LIVE_ASK_WAIT_BUDGET_MS = 10_000;
7
7
  const LIVE_ASK_POLL_DELAYS_MS = [
8
- 1_000, 1_000, 2_000, 2_000, 3_000, 3_000,
9
- 4_000, 4_000, 5_000, 5_000, 5_000, 5_000,
8
+ 1_000, 1_500, 2_500, 4_000,
10
9
  ];
11
10
  const CLAUDE_CHANNEL_POLL_INTERVAL_MS = 2_000;
12
11
  const CLAUDE_CHANNEL_MAX_WATCH_MS = 15 * 60 * 1_000;
@@ -62,6 +61,7 @@ const resultSchema = {
62
61
  verdict: z.enum(["aligned", "unaligned"]).nullable(),
63
62
  confidence: z.number().nullable(),
64
63
  latency_s: z.number().nullable(),
64
+ user_feedback: z.enum(["agreed", "disagreed", "not_provided"]).nullable().optional(),
65
65
  };
66
66
  const contextItemSchema = z.object({
67
67
  request_id: z.string(),
@@ -87,6 +87,7 @@ const liveAskSchema = {
87
87
  request_id: z.string().nullable(),
88
88
  verdict: z.enum(["aligned", "unaligned"]).nullable(),
89
89
  confidence: z.number().min(0).max(1).nullable(),
90
+ user_feedback: z.enum(["agreed", "disagreed", "not_provided"]).nullable().optional(),
90
91
  expires_at: z.string().datetime({ offset: true }).nullable(),
91
92
  idempotent_replay: z.boolean().nullable(),
92
93
  delivery_state: z.enum(["queued", "processing", "sent", "failed", "skipped"]).nullable(),
@@ -111,34 +112,18 @@ const contextShareSchema = {
111
112
  captured_at: z.string().datetime({ offset: true }),
112
113
  };
113
114
  const SERVER_INSTRUCTIONS = [
114
- "SomaCheck lets you offer the person a statement to test through a quick phone check-in.",
115
- "MANDATORY COMMAND RULE: when the current user message says 'Give me a SomaCheck vibecheck', 'Give me a check-in, a vibe check', 'Request a SomaCheck vibecheck', or otherwise explicitly asks for one, you must call request_vibecheck in that same turn after any read-only context/status calls. Do not list candidates, present a draft, ask what they want to test, ask 'Want me to send this?', or wait for another yes. The request itself is one-ask consent for you to select and send the statement.",
116
- "Call get_vibecheck_context and get_vibecheck_status at the start of a session or background run, and call status again after a completed check-in.",
117
- "Treat gesture outcomes as contextual signals, never fixed facts or blanket authorization.",
118
- "Use request_vibecheck when a current choice would benefit from the person's own felt response; never use it to evaluate someone else.",
119
- "A setup or link is not blanket consent for future vibechecks.",
120
- "If the person's current message explicitly requests a SomaCheck vibecheck, that message is consent for one ask. If they supplied exact wording, use it. Otherwise, choose one useful statement from the current conversation, goal, decision, or tension and send it immediately; do not ask them to supply wording or confirm twice.",
121
- "DELEGATED SELECTION RULE: before choosing wording, use the reliable context already available to your host: the current conversation, current working goal, and user-approved memory or project context when those capabilities exist. Do not make the person restate context you can already retrieve. Remembered task status may identify a general tension, but the statement itself must not name a project, build, branch, wave, ticket, task status, or supposedly open/closed work. Abstract it into an original present-tense first-person experience or choice. This restriction does not alter exact wording supplied by the person.",
122
- "When choosing the statement, prefer the most consequential unresolved first-person decision or tension you can support with current evidence. Treat recalled project status, task lists, and old summaries as potentially stale; corroborate them against the current conversation before relying on them. Do not reuse a stock example or recent SomaCheck statement. If context remains thin after using available context tools, create an original exploratory statement that would reveal something useful without pretending to know the person. Never diagnose, judge another person, or claim certainty about the user.",
123
- "Otherwise, first show the exact statement and ask: Want a quick vibecheck before I continue? I'll send this statement to your phone; you answer with your wrist gesture, and I'll use the result as a signal—not a verdict. Call only after the person accepts.",
124
- "Use at most one vibecheck per decision. Never re-ask to obtain a preferred answer.",
125
- "An immediate vibecheck is separate from the asynchronous reflection feed. Do not use it to replenish cached propositions.",
126
- "Use a fresh idempotency_key for each new vibecheck, and reuse that exact key only when retrying the same statement.",
127
- "request_vibecheck waits boundedly for the exact answer. If it returns completed, continue in the same turn and explain what changes. If it returns pending, retain that exact handle and read it once later; never create a replacement ask.",
128
- "If request_vibecheck returns pending, never promise that you will check back on your own. Only continue automatically after an actual SomaCheck channel event arrives for that exact request.",
129
- "After a completed result, do not merely repeat the verdict, confidence, or generic signal-not-verdict language. Reconcile the result with the current goal and state one observable change in your next action. Continue with that next reversible in-scope action when it is already authorized; if new permission is required, name the exact prepared next step and ask only for that permission. Never say 'your body decided'; a typed human response always overrides the signal.",
130
- "When you have meaningful user-authorized context, call share_somacheck_context with concise derived observations before creating propositions.",
131
- "Never share raw conversation text, photos, credentials, private identifiers, or diagnostic claims.",
132
- "Maintain three distinct propositions drawn from your own context about this person.",
133
- "When propositions_needed is greater than zero, post exactly that many in one call.",
134
- "Offer each statement as something to test, never as a claim of fact about the user.",
135
- "Retain every request_id returned by post_vibecheck_statement and call get_vibecheck_result later.",
136
- "Do not poll continuously. Queued means the proposition is cached until the person advances their feed.",
115
+ "SomaCheck lets you ask your person for a vibecheck.",
116
+ "You can proactively offer one when you believe it would be useful, or send one when the person asks.",
117
+ "When the person asks, choose and send a useful first-person statement from the context available to you; do not make them supply wording unless they want to.",
118
+ "Help the person get an insight about themselves from the context you have.",
119
+ "The returned gesture and optional feedback are context, not authorization.",
120
+ "Use your judgment in how you interpret and respond.",
121
+ "Never include secrets, raw private content, diagnostic claims, or use SomaCheck to assess anyone other than the person you are interacting with.",
137
122
  ].join(" ");
138
- const DIRECT_REQUEST_NEXT_ACTION = "DIRECT REQUEST RULE: If the person's current message asks for a SomaCheck vibecheck or check-in, first use reliable context already available to the host, including current conversation and user-approved memory when supported. Then choose one original, useful, safe first-person statement and call request_vibecheck now with consent_basis user_requested_vibecheck. Do not list choices, ask for wording, or seek another confirmation. Ignore reflection-feed propositions_needed for this direct request. Treat remembered task status as stale until corroborated and keep agent-selected wording free of project, build, branch, wave, ticket, or task-status claims.";
139
123
  export function createVibecheckServer(dependencies) {
140
124
  const supportsClaudeChannel = dependencies.identity?.kind === "local"
141
- && dependencies.identity.client_key === "claude";
125
+ && dependencies.identity.client_key === "claude"
126
+ && typeof dependencies.claudeChannelWatch === "object";
142
127
  const server = new McpServer({ name: PACKAGE_NAME, version: PACKAGE_VERSION }, {
143
128
  instructions: SERVER_INSTRUCTIONS,
144
129
  ...(supportsClaudeChannel
@@ -181,12 +166,10 @@ export function createVibecheckServer(dependencies) {
181
166
  const token = await dependencies.loadToken();
182
167
  const checkins = await dependencies.api.contextRequest(token);
183
168
  const text = checkins.length === 0
184
- ? `No completed SomaCheck gestures yet.\n\n${DIRECT_REQUEST_NEXT_ACTION}`
169
+ ? "No completed SomaCheck gestures yet."
185
170
  : [
186
- `Loaded ${checkins.length} completed gesture${checkins.length === 1 ? "" : "s"}. Treat them as contextual signals, not fixed conclusions:`,
171
+ `Loaded ${checkins.length} completed gesture${checkins.length === 1 ? "" : "s"}:`,
187
172
  ...checkins.map((item) => `${Math.round(item.confidence * 100)}% ${item.verdict} — ${JSON.stringify(item.statement)}`),
188
- "",
189
- DIRECT_REQUEST_NEXT_ACTION,
190
173
  ].join("\n");
191
174
  return { content: [{ type: "text", text }], structuredContent: { checkins } };
192
175
  }
@@ -212,7 +195,7 @@ export function createVibecheckServer(dependencies) {
212
195
  return {
213
196
  content: [{
214
197
  type: "text",
215
- text: `${summarise(status)}\n\n${DIRECT_REQUEST_NEXT_ACTION}`,
198
+ text: summarise(status),
216
199
  }],
217
200
  structuredContent: { ...status },
218
201
  };
@@ -306,7 +289,7 @@ export function createVibecheckServer(dependencies) {
306
289
  const text = result.status === "answered"
307
290
  ? liveRequestId === null
308
291
  ? `Answered: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}.`
309
- : `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Do not stop at restating the result or generic signal language. Reconcile it with the current goal, state one observable change, and continue with the next reversible in-scope action when already authorized. If new permission is required, name the exact prepared next step. A typed response from the person always overrides this signal.`
292
+ : `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
310
293
  : result.status === "queued"
311
294
  ? "Queued. The proposition is cached until the person advances their feed."
312
295
  : result.status === "pending"
@@ -320,14 +303,14 @@ export function createVibecheckServer(dependencies) {
320
303
  });
321
304
  server.registerTool("request_vibecheck", {
322
305
  title: "Request a Vibecheck",
323
- description: "Send one plain-language statement to the person's phone for a vibecheck answered with a wrist gesture. A current user message that explicitly requests a SomaCheck vibecheck is confirmation for one ask. Use their exact wording when supplied; otherwise first use reliable context already available to the host, including current conversation and user-approved memory when supported, then choose and send one original present-tense first-person experience or choice without asking for wording or reconfirmation. Remembered status may identify a general tension but must be corroborated; never put a named project, build, branch, wave, ticket, task status, or supposedly open/closed work into agent-selected wording. Do not reuse a stock example or recent SomaCheck statement. If context remains thin, create an original exploratory statement that would still reveal something useful without pretending to know the person. Otherwise, first show the exact statement and call only after the person accepts. Setup or linking is not blanket consent. Use this only when their own felt response would help with the current choice, not for routine feed content, diagnosis, or judging another person. The call waits boundedly for the exact result and returns completed when available. A timeout returns the same stable pending request_id for one later read. Caller cancellation stops result reads without creating another request; the cancelled transport may suppress the pending response.",
306
+ description: "Send one statement to the person's phone for a SomaCheck vibecheck. If the person asks for a vibecheck, choose a useful first-person statement from your available context and send it. For proactive offers, call only after the person accepts. The result is context, not authorization.",
324
307
  inputSchema: {
325
308
  statement: z.string().trim().min(1).max(1000)
326
- .describe("One plain-language statement for the person to test; offer a signal, not a fact, diagnosis, or instruction. If the agent selected the wording, it must be a present-tense first-person experience or choice and must not name remembered projects, builds, branches, waves, tickets, task status, or supposedly open/closed work. Preserve exact wording supplied by the person."),
309
+ .describe("One plain-language first-person statement for the person to test. Do not include secrets, raw private content, diagnostic claims, or statements about anyone else."),
327
310
  idempotency_key: z.string().uuid()
328
311
  .describe("A new UUID for this logical ask. Reuse the same UUID only to retry the exact same statement; retries will not create another phone request."),
329
312
  consent_basis: z.enum(["user_requested_vibecheck", "user_approved_statement"])
330
- .describe("Required one-ask consent basis. Use user_requested_vibecheck when the current message asks for a SomaCheck vibecheck, including when the agent chooses the statement; call immediately without another confirmation. Use user_approved_statement only after the agent proposed exact wording and the person accepted it."),
313
+ .describe("Use user_requested_vibecheck when the person asks for a vibecheck. Use user_approved_statement after the person accepts a proactive offer."),
331
314
  },
332
315
  outputSchema: liveAskSchema,
333
316
  annotations: {
@@ -376,12 +359,12 @@ export function createVibecheckServer(dependencies) {
376
359
  startClaudeChannelWatch(result, token, identity);
377
360
  const structuredContent = { ...result, error_code: null };
378
361
  const text = result.state === "completed"
379
- ? `Vibecheck completed: ${Math.round((result.confidence ?? 0) * 100)}% ${result.verdict}. Do not stop at restating the result or generic signal language. Reconcile it with the current goal, state one observable change, and continue with the next reversible in-scope action when already authorized. If new permission is required, name the exact prepared next step. A typed response from the person always overrides this signal.`
362
+ ? `Vibecheck completed: ${result.verdict}, confidence ${formatChannelConfidence(result.confidence)}${formatUserFeedback(result.user_feedback)}.`
380
363
  : result.state === "pending"
381
364
  ? result.delivery_state === "failed" || result.delivery_state === "skipped"
382
- ? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}. Do not repost it or change the idempotency_key; check the phone setup before a later decision.`
383
- : `Vibecheck ${result.request_id} is still pending after a bounded wait. Keep this exact handle and check it once later with get_vibecheck_result. It expires at ${result.expires_at}; do not poll continuously, re-ask for this decision, or promise an automatic follow-up unless an actual SomaCheck channel event arrives.`
384
- : `This vibecheck is ${result.state}. Do not create another ask for the same decision.`;
365
+ ? `Vibecheck ${result.request_id} was stored, but phone delivery is ${result.delivery_state}. It expires at ${result.expires_at}.`
366
+ : `Vibecheck ${result.request_id} is pending. It expires at ${result.expires_at}.`
367
+ : `This vibecheck is ${result.state}.`;
385
368
  return { content: [{ type: "text", text }], structuredContent };
386
369
  }
387
370
  catch (error) {
@@ -431,8 +414,8 @@ async function watchForClaudeChannelResult(created, token, identity, dependencie
431
414
  continue;
432
415
  const handle = `live:${requestId}`;
433
416
  const content = lifecycle.status === "answered"
434
- ? `A SomaCheck vibecheck requested by this Claude connection has completed. The exact result is ${lifecycle.verdict} with confidence ${formatChannelConfidence(lifecycle.confidence)}. Continue the current conversation now. Do not merely restate the result or give generic signal-not-verdict commentary. Reconcile it with the current goal, state one observable change in your next action, and perform the next reversible in-scope action when it is already authorized. If new permission is required, name the exact prepared next step and ask only for that permission. The exact opaque request handle is ${handle}. Do not request another vibecheck for this decision.`
435
- : `A SomaCheck vibecheck requested by this Claude connection is now ${lifecycle.status}. The exact opaque request handle is ${handle}. Do not create a replacement request for the same decision.`;
417
+ ? `SomaCheck vibecheck ${handle} completed: ${lifecycle.verdict}, confidence ${formatChannelConfidence(lifecycle.confidence)}${formatUserFeedback(lifecycle.user_feedback)}.`
418
+ : `SomaCheck vibecheck ${handle} is ${lifecycle.status}.`;
436
419
  try {
437
420
  await notify({
438
421
  method: "notifications/claude/channel",
@@ -464,6 +447,9 @@ function unrefSleep(milliseconds) {
464
447
  function formatChannelConfidence(confidence) {
465
448
  return confidence === null ? "unavailable" : confidence.toFixed(2);
466
449
  }
450
+ function formatUserFeedback(feedback) {
451
+ return feedback === undefined || feedback === null ? "" : `, user feedback ${feedback}`;
452
+ }
467
453
  async function waitForLiveVibecheck(created, token, identity, dependencies, signal, deadline, progress) {
468
454
  if (created.state !== "pending"
469
455
  || created.delivery_state === "failed"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@somacheck/vibecheck",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "Send a consented vibecheck to SomaCheck and use the result as a signal.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,7 +15,9 @@
15
15
  ".": "./dist/server.js"
16
16
  },
17
17
  "files": [
18
- "dist/*.js"
18
+ "dist/*.js",
19
+ "SKILL.md",
20
+ "claude-marketplace/**"
19
21
  ],
20
22
  "scripts": {
21
23
  "build": "rm -rf dist && tsc -p tsconfig.json",