@pcircle/memesh 4.9.4 → 4.10.2
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +75 -13
- package/README.de.md +5 -5
- package/README.md +5 -5
- package/README.zh-TW.md +5 -5
- package/dashboard/dist/index.html +8 -8
- package/dist/cli/view-live.d.ts.map +1 -1
- package/dist/cli/view-live.js +0 -427
- package/dist/cli/view-live.js.map +1 -1
- package/dist/core/agent-message-inbox.d.ts +6 -0
- package/dist/core/agent-message-inbox.d.ts.map +1 -1
- package/dist/core/agent-message-inbox.js +28 -1
- package/dist/core/agent-message-inbox.js.map +1 -1
- package/dist/core/briefing-index.d.ts +30 -0
- package/dist/core/briefing-index.d.ts.map +1 -0
- package/dist/core/briefing-index.js +140 -0
- package/dist/core/briefing-index.js.map +1 -0
- package/dist/core/briefing-level.d.ts +22 -0
- package/dist/core/briefing-level.d.ts.map +1 -0
- package/dist/core/briefing-level.js +75 -0
- package/dist/core/briefing-level.js.map +1 -0
- package/dist/core/briefing.d.ts +7 -0
- package/dist/core/briefing.d.ts.map +1 -1
- package/dist/core/briefing.js +72 -12
- package/dist/core/briefing.js.map +1 -1
- package/dist/core/capture-liveness.d.ts +137 -0
- package/dist/core/capture-liveness.d.ts.map +1 -0
- package/dist/core/capture-liveness.js +448 -0
- package/dist/core/capture-liveness.js.map +1 -0
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +5 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/delegation.d.ts +47 -0
- package/dist/core/delegation.d.ts.map +1 -0
- package/dist/core/delegation.js +174 -0
- package/dist/core/delegation.js.map +1 -0
- package/dist/core/doctor-fixes.d.ts +1 -0
- package/dist/core/doctor-fixes.d.ts.map +1 -1
- package/dist/core/doctor-fixes.js +21 -0
- package/dist/core/doctor-fixes.js.map +1 -1
- package/dist/core/doctor.d.ts +8 -0
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +148 -3
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/note-derive.d.ts +14 -0
- package/dist/core/note-derive.d.ts.map +1 -0
- package/dist/core/note-derive.js +66 -0
- package/dist/core/note-derive.js.map +1 -0
- package/dist/core/note-ingest.d.ts +34 -0
- package/dist/core/note-ingest.d.ts.map +1 -0
- package/dist/core/note-ingest.js +445 -0
- package/dist/core/note-ingest.js.map +1 -0
- package/dist/core/operations.d.ts +10 -1
- package/dist/core/operations.d.ts.map +1 -1
- package/dist/core/operations.js +129 -14
- package/dist/core/operations.js.map +1 -1
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +4 -0
- package/dist/core/paths.js.map +1 -1
- package/dist/core/schema-export.d.ts.map +1 -1
- package/dist/core/schema-export.js +15 -5
- package/dist/core/schema-export.js.map +1 -1
- package/dist/core/serializer.d.ts +2 -0
- package/dist/core/serializer.d.ts.map +1 -1
- package/dist/core/serializer.js +119 -3
- package/dist/core/serializer.js.map +1 -1
- package/dist/core/session-insight.d.ts +44 -0
- package/dist/core/session-insight.d.ts.map +1 -0
- package/dist/core/session-insight.js +216 -0
- package/dist/core/session-insight.js.map +1 -0
- package/dist/core/task-state-store.d.ts +4 -0
- package/dist/core/task-state-store.d.ts.map +1 -1
- package/dist/core/task-state-store.js +18 -5
- package/dist/core/task-state-store.js.map +1 -1
- package/dist/core/task-state.d.ts +5 -0
- package/dist/core/task-state.d.ts.map +1 -1
- package/dist/core/task-state.js +70 -0
- package/dist/core/task-state.js.map +1 -1
- package/dist/core/turn-signal.d.ts +22 -0
- package/dist/core/turn-signal.d.ts.map +1 -0
- package/dist/core/turn-signal.js +89 -0
- package/dist/core/turn-signal.js.map +1 -0
- package/dist/core/types.d.ts +14 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/update-entrypoint.d.ts +19 -0
- package/dist/core/update-entrypoint.d.ts.map +1 -0
- package/dist/core/update-entrypoint.js +222 -0
- package/dist/core/update-entrypoint.js.map +1 -0
- package/dist/core/update-notice.d.ts +69 -0
- package/dist/core/update-notice.d.ts.map +1 -0
- package/dist/core/update-notice.js +188 -0
- package/dist/core/update-notice.js.map +1 -0
- package/dist/core/work-topology.d.ts +1 -0
- package/dist/core/work-topology.d.ts.map +1 -1
- package/dist/core/work-topology.js +3 -0
- package/dist/core/work-topology.js.map +1 -1
- package/dist/host-runtime/codex-session.d.ts +10 -0
- package/dist/host-runtime/codex-session.d.ts.map +1 -1
- package/dist/host-runtime/codex-session.js +1 -1
- package/dist/host-runtime/codex-session.js.map +1 -1
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +24 -3
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
- package/dist/mcp/server.js +1438 -122
- package/dist/mcp/server.js.map +3 -3
- package/dist/skills-manifest.json +61 -36
- package/dist/storage/fts-index.d.ts +1 -0
- package/dist/storage/fts-index.d.ts.map +1 -1
- package/dist/storage/fts-index.js +5 -0
- package/dist/storage/fts-index.js.map +1 -1
- package/dist/transports/cli/cli.d.ts.map +1 -1
- package/dist/transports/cli/cli.js +7846 -5613
- package/dist/transports/cli/cli.js.map +3 -3
- package/dist/transports/http/server.d.ts.map +1 -1
- package/dist/transports/http/server.js +48 -28
- package/dist/transports/http/server.js.map +1 -1
- package/dist/transports/mcp/handlers.d.ts +31 -7
- package/dist/transports/mcp/handlers.d.ts.map +1 -1
- package/dist/transports/mcp/handlers.js +49 -7
- package/dist/transports/mcp/handlers.js.map +1 -1
- package/dist/transports/schemas.d.ts +5 -2
- package/dist/transports/schemas.d.ts.map +1 -1
- package/dist/transports/schemas.js +30 -3
- package/dist/transports/schemas.js.map +1 -1
- package/docs/platforms/README.md +1 -0
- package/docs/platforms/agent-messaging.md +39 -8
- package/package.json +11 -6
- package/scripts/hooks/_generated/agent-message-inbox.js +28 -1
- package/scripts/hooks/_generated/briefing-index.js +147 -0
- package/scripts/hooks/_generated/briefing-level.js +82 -0
- package/scripts/hooks/_generated/capture-liveness.js +455 -0
- package/scripts/hooks/_generated/core-paths.js +4 -0
- package/scripts/hooks/_generated/fts-index.js +5 -0
- package/scripts/hooks/_generated/task-state.js +70 -0
- package/scripts/hooks/_generated/update-notice.js +195 -0
- package/scripts/hooks/_generated/work-topology.js +3 -0
- package/scripts/hooks/_shared.js +923 -27
- package/scripts/hooks/_stop-notes.js +504 -0
- package/scripts/hooks/auto-update-runner.mjs +22 -0
- package/scripts/hooks/decision-nudge.js +32 -5
- package/scripts/hooks/guard-check.js +30 -3
- package/scripts/hooks/post-commit.js +329 -133
- package/scripts/hooks/pre-compact.js +17 -2
- package/scripts/hooks/pre-edit-recall.js +416 -64
- package/scripts/hooks/session-start.js +617 -150
- package/scripts/hooks/session-summary.js +217 -41
- package/scripts/hooks/user-prompt-intent.js +124 -22
- package/scripts/upgrade-plugin.sh +3 -4
- package/skills/memesh/SKILL.md +45 -23
- package/dist/cli/assets/d3.v7.min.js +0 -2
- package/dist/core/graph.d.ts +0 -26
- package/dist/core/graph.d.ts.map +0 -1
- package/dist/core/graph.js +0 -74
- package/dist/core/graph.js.map +0 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
import { createRequire } from 'module';
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
|
-
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { existsSync, readFileSync, writeSync } from 'fs';
|
|
10
10
|
import { pathToFileURL } from 'url';
|
|
11
11
|
import {
|
|
12
12
|
AUTO_CAPTURE_TAG,
|
|
@@ -20,13 +20,17 @@ import {
|
|
|
20
20
|
openHookDb,
|
|
21
21
|
readUpdateCheckCache,
|
|
22
22
|
redactSecrets,
|
|
23
|
+
hookErrorReason,
|
|
24
|
+
recordHookOutcome,
|
|
23
25
|
recordHookRun,
|
|
24
26
|
stampHookRunOnly,
|
|
25
27
|
resolveAutoUpdatePolicy,
|
|
26
28
|
resolvePluginRoot,
|
|
29
|
+
SKIP_REASONS,
|
|
27
30
|
spawnAutoUpdate,
|
|
28
31
|
truncateTitle,
|
|
29
32
|
} from './_shared.js';
|
|
33
|
+
import { runStopNotes } from './_stop-notes.js';
|
|
30
34
|
|
|
31
35
|
const require = createRequire(import.meta.url);
|
|
32
36
|
|
|
@@ -211,13 +215,22 @@ function parseTranscript(transcriptPath) {
|
|
|
211
215
|
let input = '';
|
|
212
216
|
process.stdin.setEncoding('utf8');
|
|
213
217
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
218
|
+
// See post-commit.js for why every exit path leaves a record (#327).
|
|
219
|
+
let payload = null;
|
|
220
|
+
let pendingSystemMessage = null;
|
|
221
|
+
/** Set by runStopNotes; called by exit0() with whether the nudge went out. */
|
|
222
|
+
let settleNudge = null;
|
|
223
|
+
function record(outcome, reason, entity) {
|
|
224
|
+
recordHookOutcome(process.env, { hook: 'session-summary', outcome, reason, entity, payload });
|
|
225
|
+
}
|
|
226
|
+
|
|
214
227
|
process.stdin.on('end', async () => {
|
|
215
228
|
let sessionId = 'unknown';
|
|
216
229
|
try {
|
|
217
|
-
if (!input.trim())
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
if (!input.trim()) {
|
|
231
|
+
record('skipped', SKIP_REASONS.emptyStdin);
|
|
232
|
+
return exit0();
|
|
233
|
+
}
|
|
221
234
|
|
|
222
235
|
let inputData;
|
|
223
236
|
try {
|
|
@@ -230,12 +243,37 @@ process.stdin.on('end', async () => {
|
|
|
230
243
|
const preview = (input || '').slice(0, 80).replace(/\n/g, ' ');
|
|
231
244
|
process.stderr.write(`[memesh session-summary] malformed stdin JSON (len=${input.length}): ${parseErr?.message || parseErr}; preview="${preview}"\n`);
|
|
232
245
|
} catch {}
|
|
246
|
+
record('error', 'malformed stdin JSON');
|
|
233
247
|
return exit0();
|
|
234
248
|
}
|
|
235
249
|
|
|
250
|
+
payload = inputData;
|
|
236
251
|
sessionId = inputData.session_id || 'unknown';
|
|
237
252
|
const transcriptPath = inputData.transcript_path;
|
|
238
253
|
|
|
254
|
+
// Opt-out check (env > config > default-on). Read before the note work
|
|
255
|
+
// below because ingestion WRITES memories and must honour it; the nudge
|
|
256
|
+
// writes nothing and runs either way (#324). It used to sit above the
|
|
257
|
+
// JSON parse, which is why the payload is parsed first now.
|
|
258
|
+
const captureEnabled = isAutoCaptureEnabled(process.env);
|
|
259
|
+
|
|
260
|
+
// #324: ingest the project's note directory and decide the
|
|
261
|
+
// "decided things, stored nothing" nudge. Records its own outcomes under
|
|
262
|
+
// `note-ingest` / `remember-nudge`, never throws, and only ever yields
|
|
263
|
+
// one line for exit0() to print.
|
|
264
|
+
const stopNotes = await runStopNotes(inputData, {
|
|
265
|
+
captureEnabled,
|
|
266
|
+
project: inputData.cwd ? getProjectName(inputData.cwd) : undefined,
|
|
267
|
+
metaUrl: import.meta.url,
|
|
268
|
+
});
|
|
269
|
+
pendingSystemMessage = stopNotes.message;
|
|
270
|
+
settleNudge = stopNotes.settle;
|
|
271
|
+
|
|
272
|
+
if (!captureEnabled) {
|
|
273
|
+
record('skipped', SKIP_REASONS.autoCaptureOff);
|
|
274
|
+
return exit0();
|
|
275
|
+
}
|
|
276
|
+
|
|
239
277
|
// `cwd` decides the project tag, and the project tag decides which
|
|
240
278
|
// sessions `session-start` injects and which memories `pre-edit-recall`
|
|
241
279
|
// surfaces. Falling back to `process.cwd()` — the hook process's launch
|
|
@@ -249,6 +287,7 @@ process.stdin.on('end', async () => {
|
|
|
249
287
|
// capture than to file it under the wrong project. Same rule here.
|
|
250
288
|
if (!inputData.cwd) {
|
|
251
289
|
try { process.stderr.write(`[memesh session-summary] cwd absent in payload (keys: ${Object.keys(inputData).join(',')}); cannot resolve project, skipping capture\n`); } catch {}
|
|
290
|
+
record('skipped', SKIP_REASONS.cwdAbsent);
|
|
252
291
|
return exit0();
|
|
253
292
|
}
|
|
254
293
|
const cwd = inputData.cwd;
|
|
@@ -279,7 +318,11 @@ process.stdin.on('end', async () => {
|
|
|
279
318
|
// (empty stdin, malformed JSON, missing cwd) are schema-flip shapes: if
|
|
280
319
|
// Claude Code's payload changed under us, capture is effectively dead,
|
|
281
320
|
// and a heartbeat would mask exactly that.
|
|
282
|
-
if (!wasAgenticLoop) {
|
|
321
|
+
if (!wasAgenticLoop) {
|
|
322
|
+
stampHookRunOnly(process.env, 'session-summary');
|
|
323
|
+
record('skipped', SKIP_REASONS.notAgenticLoop);
|
|
324
|
+
return exit0();
|
|
325
|
+
}
|
|
283
326
|
// Trace why we're skipping. Two failure modes:
|
|
284
327
|
// (a) transcript_path absent — schema flip, Claude Code stopped
|
|
285
328
|
// sending the field. Same bug shape as `was_in_agentic_loop`
|
|
@@ -291,6 +334,7 @@ process.stdin.on('end', async () => {
|
|
|
291
334
|
// breadcrumb so a schema flip doesn't ship undetected again.
|
|
292
335
|
if (!transcriptPath) {
|
|
293
336
|
try { process.stderr.write(`[memesh session-summary] transcript_path absent in payload (keys: ${Object.keys(inputData).join(',')}); skipping capture\n`); } catch {}
|
|
337
|
+
record('skipped', SKIP_REASONS.transcriptPathAbsent);
|
|
294
338
|
return exit0();
|
|
295
339
|
}
|
|
296
340
|
if (!existsSync(transcriptPath)) {
|
|
@@ -299,6 +343,7 @@ process.stdin.on('end', async () => {
|
|
|
299
343
|
// race) — the hook itself ran fine, so this stamps. A payload that
|
|
300
344
|
// never carried the field at all (schema flip) bails above, unstamped.
|
|
301
345
|
stampHookRunOnly(process.env, 'session-summary');
|
|
346
|
+
record('skipped', SKIP_REASONS.transcriptGone);
|
|
302
347
|
return exit0();
|
|
303
348
|
}
|
|
304
349
|
|
|
@@ -310,11 +355,18 @@ process.stdin.on('end', async () => {
|
|
|
310
355
|
// LOST (permissions, I/O), and a heartbeat here would keep doctor green
|
|
311
356
|
// through exactly the repeated failure it exists to expose. No stamp —
|
|
312
357
|
// parseTranscript already traced the fault to stderr.
|
|
313
|
-
if (readFailed)
|
|
358
|
+
if (readFailed) {
|
|
359
|
+
record('error', 'the transcript could not be read');
|
|
360
|
+
return exit0();
|
|
361
|
+
}
|
|
314
362
|
|
|
315
363
|
// Skip sessions with too little activity — the single most common
|
|
316
364
|
// healthy exit, so it MUST stamp (see stampHookRunOnly).
|
|
317
|
-
if (toolCallCount < 3) {
|
|
365
|
+
if (toolCallCount < 3) {
|
|
366
|
+
stampHookRunOnly(process.env, 'session-summary');
|
|
367
|
+
record('skipped', SKIP_REASONS.tooLittleActivity);
|
|
368
|
+
return exit0();
|
|
369
|
+
}
|
|
318
370
|
|
|
319
371
|
const projectName = getProjectName(cwd);
|
|
320
372
|
|
|
@@ -324,35 +376,23 @@ process.stdin.on('end', async () => {
|
|
|
324
376
|
//
|
|
325
377
|
const { db } = openHookDb(process.env, { fts: true });
|
|
326
378
|
let writeFailed = false;
|
|
379
|
+
let firstFailedEntity = null;
|
|
380
|
+
// True once any of the three rules below actually calls storeMemory.
|
|
381
|
+
// Between the toolCallCount < 3 guard above and Rule 3's >= 20 bar, a
|
|
382
|
+
// session that ran real commands but edited no file matches none of
|
|
383
|
+
// them — storeMemory never runs, writeFailed stays false, and without
|
|
384
|
+
// this flag the outcome below fell through to record('wrote') anyway:
|
|
385
|
+
// a claimed write with zero entities actually touched.
|
|
386
|
+
let anyRuleMatched = false;
|
|
387
|
+
// True only once captureEntity actually lands a write. A matched rule
|
|
388
|
+
// whose entity is `forget`-archived sets anyRuleMatched but not this —
|
|
389
|
+
// the archived branch below returns before either flag changes, so a
|
|
390
|
+
// Stop where every matched rule's target was archived falls through to
|
|
391
|
+
// the `!anyWrote` branch instead of a false 'wrote' (same bug shape as
|
|
392
|
+
// the noRuleMatched fix above, one level deeper).
|
|
393
|
+
let anyWrote = false;
|
|
394
|
+
let lastWrittenEntity = null;
|
|
327
395
|
try {
|
|
328
|
-
// Duplicate detection: if we already captured this session, bail.
|
|
329
|
-
//
|
|
330
|
-
// Use the FULL session_id rather than the first 8 chars: real
|
|
331
|
-
// Claude Code UUIDs collide on 8 chars only with cosmically small
|
|
332
|
-
// probability, but artificial test IDs (verify-fix-001 vs -002)
|
|
333
|
-
// share the prefix and silently skipped the second session
|
|
334
|
-
// entirely. The contract is one stored capture per distinct
|
|
335
|
-
// session_id, so the dedup key has to be the full id.
|
|
336
|
-
//
|
|
337
|
-
// A dedup bail is a SUCCESSFUL run — the loop executed and correctly
|
|
338
|
-
// decided there was nothing to do — so it stamps the heartbeat like
|
|
339
|
-
// the capture path below does. Only a throw leaves no stamp.
|
|
340
|
-
// Guard on ANY of this session's three entities, not only `-files`.
|
|
341
|
-
// A Bash-only session created no `-files` row, so the guard never
|
|
342
|
-
// tripped and `-summary` was re-appended on every Stop — measured: 56
|
|
343
|
-
// observations, 16 unique, three commands stored fourteen times each.
|
|
344
|
-
const alreadyCaptured = db.prepare(
|
|
345
|
-
"SELECT id FROM entities WHERE name IN (?, ?, ?) LIMIT 1",
|
|
346
|
-
).get(`session-${sessionId}-files`, `session-${sessionId}-fixes`, `session-${sessionId}-summary`);
|
|
347
|
-
if (alreadyCaptured) {
|
|
348
|
-
recordHookRun(db, 'session-summary');
|
|
349
|
-
// A duplicate capture is still a completed Stop lifecycle. Update
|
|
350
|
-
// consent is session-scoped and must not be skipped merely because
|
|
351
|
-
// the same transcript was observed twice (a common host retry).
|
|
352
|
-
await runAutoUpdateAtStop(sessionId);
|
|
353
|
-
return exit0();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
396
|
// Build and store session memories
|
|
357
397
|
const baseTags = [AUTO_CAPTURE_TAG, `session:${sessionId}`, `project:${projectName}`];
|
|
358
398
|
|
|
@@ -378,12 +418,51 @@ process.stdin.on('end', async () => {
|
|
|
378
418
|
// entities_fts too. This copy used to insert entity + observations + tags
|
|
379
419
|
// only, skipping the FTS reindex the sibling hooks did — which left every
|
|
380
420
|
// session-insight memory unrecallable via the FTS keyword path.
|
|
421
|
+
//
|
|
422
|
+
// Known tradeoff, not a bug: the three Rule blocks below each call this
|
|
423
|
+
// function independently, and captureEntity() commits its own
|
|
424
|
+
// transaction per call. A failure partway through Rule 2 or 3 can leave
|
|
425
|
+
// an earlier entity (e.g. -files) replaced while a later one is not,
|
|
426
|
+
// even though the overall Stop is recorded as 'error'. Wrapping all
|
|
427
|
+
// three in one outer db.transaction() would close that gap (nested
|
|
428
|
+
// calls become SAVEPOINTs — see MemeshDatabase.transaction() in
|
|
429
|
+
// src/storage/sqlite.ts) but was deliberately not done here: it widens
|
|
430
|
+
// the write-lock hold on every Stop (this hook's busy_timeout is
|
|
431
|
+
// shorter than the harness timeout on purpose), to guard a failure mode
|
|
432
|
+
// that self-heals — the next Stop rebuilds each entity fresh from the
|
|
433
|
+
// transcript, since these are snapshots, not accumulations.
|
|
381
434
|
function storeMemory(name, type, observations, tags, title) {
|
|
435
|
+
anyRuleMatched = true;
|
|
436
|
+
// `replace`: these three entities are a SNAPSHOT of one session, and
|
|
437
|
+
// Stop fires at the end of every turn. Appending stored the same
|
|
438
|
+
// sentences on every turn; skipping after the first froze a two-day
|
|
439
|
+
// session at its first turn (#322). A snapshot is restated, not added
|
|
440
|
+
// to.
|
|
441
|
+
const result = captureEntity(db, { name, type, observations, tags, title, replace: true });
|
|
442
|
+
if (result?.archived) {
|
|
443
|
+
// The user `forget`-archived this exact entity. Not a failure —
|
|
444
|
+
// captureEntity's contract left it untouched on purpose — so it
|
|
445
|
+
// must not set writeFailed (that would misreport an honoured
|
|
446
|
+
// `forget` as a broken hook). Traced, not silent (#3d): the next
|
|
447
|
+
// Stop will try again and say the same thing until the user either
|
|
448
|
+
// reactivates the entity or the session ends.
|
|
449
|
+
try { process.stderr.write(`MeMesh: session-summary left "${name}" alone — archived by forget.\n`); } catch {}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
382
452
|
// null = the entity row could not be resolved = this write did NOT
|
|
383
453
|
// happen (captureEntity's contract). A run with a failed write must
|
|
384
454
|
// not stamp the heartbeat below — "alive" would be a lie about the
|
|
385
455
|
// exact thing the heartbeat certifies.
|
|
386
|
-
if (!
|
|
456
|
+
if (!result) {
|
|
457
|
+
writeFailed = true;
|
|
458
|
+
// First failure, not last: with three independent per-entity
|
|
459
|
+
// transactions, the first is the root cause — later calls run
|
|
460
|
+
// regardless and naming one of them would point at a symptom.
|
|
461
|
+
if (firstFailedEntity === null) firstFailedEntity = name;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
anyWrote = true;
|
|
465
|
+
lastWrittenEntity = name;
|
|
387
466
|
}
|
|
388
467
|
|
|
389
468
|
// No free-form human text exists for these three entities the way a
|
|
@@ -393,7 +472,8 @@ process.stdin.on('end', async () => {
|
|
|
393
472
|
const titleDate = new Date().toISOString().slice(0, 10);
|
|
394
473
|
const titlePrefix = `${titleDate} ${projectName}`;
|
|
395
474
|
|
|
396
|
-
// Rule 1: File editing session summary
|
|
475
|
+
// Rule 1: File editing session summary — name uses the FULL
|
|
476
|
+
// session_id (tests/core/extractor.test.ts pins why).
|
|
397
477
|
if (filesEdited.length > 0) {
|
|
398
478
|
storeMemory(
|
|
399
479
|
`session-${sessionId}-files`,
|
|
@@ -421,7 +501,15 @@ process.stdin.on('end', async () => {
|
|
|
421
501
|
);
|
|
422
502
|
}
|
|
423
503
|
|
|
424
|
-
// Rule 3: Heavy session summary (20+ tool calls = significant work)
|
|
504
|
+
// Rule 3: Heavy session summary (20+ tool calls = significant work).
|
|
505
|
+
// This literal is the one place that actually decides the bar; two
|
|
506
|
+
// doc strings describe it in prose without importing it (this file is
|
|
507
|
+
// plain JS with no shared constant module, and capture-liveness.ts is
|
|
508
|
+
// a deliberate zero-import leaf) — src/core/capture-liveness.ts's
|
|
509
|
+
// SKIP_REASONS.noRuleMatched and src/core/session-insight.ts's own
|
|
510
|
+
// (HEAVY_SESSION_TOOL_CALLS-derived) copy. A future change to this
|
|
511
|
+
// number needs both updated by hand, or doctor's text will drift from
|
|
512
|
+
// what actually happened.
|
|
425
513
|
if (toolCallCount >= 20) {
|
|
426
514
|
storeMemory(
|
|
427
515
|
`session-${sessionId}-summary`,
|
|
@@ -548,9 +636,42 @@ process.stdin.on('end', async () => {
|
|
|
548
636
|
// session so it survives DB restores from either era) plus
|
|
549
637
|
// the compliance denominators: sessions that HAD an injection
|
|
550
638
|
// vs sessions whose transcript carried any citation marker.
|
|
639
|
+
//
|
|
640
|
+
// The counters are scoped to the generation named in the stamp.
|
|
641
|
+
// When a graph that counted under an EARLIER generation meets
|
|
642
|
+
// this one, they are cleared rather than added to: the
|
|
643
|
+
// numerator changed meaning (v1 asked "did the transcript
|
|
644
|
+
// contain any marker", v2 asks "was an id we injected cited"),
|
|
645
|
+
// so a sum across both is one ratio wearing the newer label and
|
|
646
|
+
// no key separates the eras. Readers — analytics.ts and
|
|
647
|
+
// scripts/audit/measure-signals.mjs — read the bare keys and
|
|
648
|
+
// therefore keep working unchanged; what they report is now
|
|
649
|
+
// this generation only. A graph that never counted has no
|
|
650
|
+
// stamp, so nothing resets on a new install.
|
|
651
|
+
const ACCOUNTING_MODE = 'citation-v2 since 2026-09-12';
|
|
652
|
+
const priorMode = db.prepare(
|
|
653
|
+
"SELECT value FROM memesh_metadata WHERE key = 'recall_accounting_mode'"
|
|
654
|
+
).get()?.value;
|
|
655
|
+
if (priorMode && priorMode !== ACCOUNTING_MODE) {
|
|
656
|
+
// Traced, not silently dropped: the numbers being discarded
|
|
657
|
+
// are the only record of the previous era, and a reset that
|
|
658
|
+
// leaves no result record is the silent-skip shape.
|
|
659
|
+
const prior = (key) => db.prepare('SELECT value FROM memesh_metadata WHERE key = ?').get(key)?.value ?? 'absent';
|
|
660
|
+
const priorTotal = prior('citation_sessions_total');
|
|
661
|
+
const priorCited = prior('citation_sessions_cited');
|
|
662
|
+
db.prepare(
|
|
663
|
+
"DELETE FROM memesh_metadata WHERE key IN ('citation_sessions_total', 'citation_sessions_cited')"
|
|
664
|
+
).run();
|
|
665
|
+
try {
|
|
666
|
+
process.stderr.write(
|
|
667
|
+
`[memesh session-summary] citation accounting generation changed (${priorMode} -> ${ACCOUNTING_MODE}); ` +
|
|
668
|
+
`counters reset from total=${priorTotal} cited=${priorCited} — the two eras count different things and are not comparable.\n`,
|
|
669
|
+
);
|
|
670
|
+
} catch {}
|
|
671
|
+
}
|
|
551
672
|
db.prepare(
|
|
552
673
|
'INSERT OR REPLACE INTO memesh_metadata (key, value) VALUES (?, ?)'
|
|
553
|
-
).run('recall_accounting_mode',
|
|
674
|
+
).run('recall_accounting_mode', ACCOUNTING_MODE);
|
|
554
675
|
const bump = db.prepare(
|
|
555
676
|
`INSERT INTO memesh_metadata (key, value) VALUES (?, '1')
|
|
556
677
|
ON CONFLICT(key) DO UPDATE SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)`
|
|
@@ -584,7 +705,39 @@ process.stdin.on('end', async () => {
|
|
|
584
705
|
// write did not land must not read as alive. (The recall-effectiveness
|
|
585
706
|
// block catches its own errors — session memories were already stored
|
|
586
707
|
// by then, so the run still counts.)
|
|
587
|
-
if (
|
|
708
|
+
if (writeFailed) {
|
|
709
|
+
// Name the entity whose captureEntity call actually returned null —
|
|
710
|
+
// not a fixed guess. With three independent per-entity writes, a
|
|
711
|
+
// hardcoded name here would point at the wrong one whenever the
|
|
712
|
+
// failure was in Rule 1 or 2.
|
|
713
|
+
record('error', 'captureEntity did not land the write', firstFailedEntity ?? undefined);
|
|
714
|
+
} else if (!anyRuleMatched) {
|
|
715
|
+
// Correctly deciding there was nothing to capture is still a
|
|
716
|
+
// completed run — same stance as the tooLittleActivity skip above,
|
|
717
|
+
// which stamps too. What it must NOT do is claim 'wrote': that was
|
|
718
|
+
// this hook's shape for every real-work-but-no-file-edit session
|
|
719
|
+
// until this branch existed.
|
|
720
|
+
recordHookRun(db, 'session-summary');
|
|
721
|
+
// No entity named: by definition no rule matched, so `-files`,
|
|
722
|
+
// `-fixes` and `-summary` are all equally untouched this Stop —
|
|
723
|
+
// naming one of them would misreport which entity this record is
|
|
724
|
+
// about.
|
|
725
|
+
record('skipped', SKIP_REASONS.noRuleMatched);
|
|
726
|
+
} else if (!anyWrote) {
|
|
727
|
+
// A rule DID match, but every entity it targeted was `forget`-
|
|
728
|
+
// archived — the same false-'wrote' shape as the branch above, one
|
|
729
|
+
// level deeper (a matched rule that produced no write). Its own
|
|
730
|
+
// reason, not noRuleMatched: a rule fired, saying otherwise would
|
|
731
|
+
// hide that.
|
|
732
|
+
recordHookRun(db, 'session-summary');
|
|
733
|
+
record('skipped', SKIP_REASONS.allMatchedEntitiesArchived);
|
|
734
|
+
} else {
|
|
735
|
+
recordHookRun(db, 'session-summary');
|
|
736
|
+
// Name the entity that actually landed the write (the last one, if
|
|
737
|
+
// more than one rule wrote) — not a fixed guess at which of the
|
|
738
|
+
// three this Stop touched.
|
|
739
|
+
record('wrote', undefined, lastWrittenEntity ?? undefined);
|
|
740
|
+
}
|
|
588
741
|
} finally {
|
|
589
742
|
db.close();
|
|
590
743
|
}
|
|
@@ -596,6 +749,7 @@ process.stdin.on('end', async () => {
|
|
|
596
749
|
// failures from this hook; with that branch gone, real capture errors stay
|
|
597
750
|
// visible without crashing the host session.
|
|
598
751
|
try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
|
|
752
|
+
record('error', hookErrorReason(err));
|
|
599
753
|
}
|
|
600
754
|
|
|
601
755
|
// Update only after all session work so installed files cannot change while
|
|
@@ -619,6 +773,28 @@ process.stdin.on('end', async () => {
|
|
|
619
773
|
});
|
|
620
774
|
|
|
621
775
|
function exit0() {
|
|
776
|
+
// The one thing this hook may print (#324): the nudge. `systemMessage` is
|
|
777
|
+
// the only Stop output Claude Code shows the user (Stop has no
|
|
778
|
+
// hookSpecificOutput variant — tests/helpers/hook-output-contract.ts).
|
|
779
|
+
// Codex's acceptance of it on Stop is NOT verified against a live Codex in
|
|
780
|
+
// this repository; tests/hooks/cross-host-output-contract.test.ts pins the
|
|
781
|
+
// exact envelope so a rejection report maps to one line. `suppressOutput`,
|
|
782
|
+
// which Codex did reject, stays gone. writeSync, not console.log: stdout is a
|
|
783
|
+
// pipe, and an async pipe write can be cut off by process.exit on macOS.
|
|
784
|
+
//
|
|
785
|
+
// The nudge's outcome is decided HERE, by whether the write succeeded —
|
|
786
|
+
// not by runStopNotes, which cannot know. A host that closed stdout gets
|
|
787
|
+
// an `error` record and keeps its transcript offset, so the next Stop
|
|
788
|
+
// judges the same window again instead of losing it to a line nobody read.
|
|
789
|
+
let delivered = true;
|
|
790
|
+
if (pendingSystemMessage) {
|
|
791
|
+
try {
|
|
792
|
+
writeSync(1, `${JSON.stringify({ systemMessage: pendingSystemMessage })}\n`);
|
|
793
|
+
} catch {
|
|
794
|
+
delivered = false; // host closed stdout; the record says so.
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
try { settleNudge?.(delivered); } catch { /* diagnostics never take the hook down */ }
|
|
622
798
|
process.exit(0);
|
|
623
799
|
}
|
|
624
800
|
/**
|
|
@@ -21,14 +21,23 @@
|
|
|
21
21
|
import { pathToFileURL } from 'url';
|
|
22
22
|
import {
|
|
23
23
|
findAutoUpdateConsent,
|
|
24
|
+
importFromPluginRoot,
|
|
24
25
|
isAutoCaptureEnabled,
|
|
26
|
+
hookErrorReason,
|
|
27
|
+
SKIP_REASONS,
|
|
28
|
+
recordHookOutcome,
|
|
29
|
+
markUpdatePromptAnswered,
|
|
30
|
+
memeshDir,
|
|
25
31
|
parseAutoUpdateConsent,
|
|
26
32
|
readUpdateCheckCache,
|
|
33
|
+
readUpdatePromptClaim,
|
|
27
34
|
resolvePluginRoot,
|
|
35
|
+
unreadMessageLines,
|
|
28
36
|
writeAutoUpdateConsent,
|
|
37
|
+
writeSnooze,
|
|
29
38
|
} from './_shared.js';
|
|
30
39
|
import { join } from 'path';
|
|
31
|
-
import { existsSync, readFileSync } from 'fs';
|
|
40
|
+
import { existsSync, readFileSync, realpathSync } from 'fs';
|
|
32
41
|
|
|
33
42
|
let installChannelMod = null;
|
|
34
43
|
try {
|
|
@@ -56,24 +65,64 @@ function currentInstalledVersion() {
|
|
|
56
65
|
} catch { return null; }
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Record the owner's answer to this session's first-use update notice.
|
|
70
|
+
*
|
|
71
|
+
* Returns 'approved' | 'declined' | 'never' | null. The answer counts only
|
|
72
|
+
* when THIS session was actually shown the notice (a prompt claim exists for
|
|
73
|
+
* session/current/latest), so a stray "no" in unrelated conversation is not
|
|
74
|
+
* a decision about updates.
|
|
75
|
+
*
|
|
76
|
+
* approved — npm-global only (the one channel with a hook-owned installer);
|
|
77
|
+
* other channels were told their manual action and cannot be
|
|
78
|
+
* approved into an install.
|
|
79
|
+
* declined — "Not now": escalating snooze for this target version, on
|
|
80
|
+
* every channel. A distinct newer target is offered again.
|
|
81
|
+
* never — "Never ask again": config.updateCheck = false; also snoozed so
|
|
82
|
+
* a host that already read the old config stays quiet.
|
|
83
|
+
*/
|
|
84
|
+
async function recordUpdateConsent(sessionId, prompt) {
|
|
60
85
|
const current = currentInstalledVersion();
|
|
61
86
|
if (!current || !sessionId) return null;
|
|
62
|
-
const channel = currentInstallChannel();
|
|
63
|
-
// Only npm-global has a hook-owned installer. Other channels receive an
|
|
64
|
-
// actionable notice at SessionStart and must not turn an "Upgrade" word
|
|
65
|
-
// into a misleading approval marker for a different installation path.
|
|
66
|
-
if (channel !== 'npm-global') return null;
|
|
67
87
|
const cache = readUpdateCheckCache(current);
|
|
68
88
|
const latest = cache?.latestVersion;
|
|
69
89
|
if (typeof latest !== 'string' || !latest) return null;
|
|
70
|
-
const pending = findAutoUpdateConsent(sessionId, current, latest, channel);
|
|
71
|
-
if (!pending || !['pending'].includes(pending.decision)) return null;
|
|
72
90
|
const decision = parseAutoUpdateConsent(prompt);
|
|
73
91
|
if (!decision) return null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const channel = currentInstallChannel();
|
|
93
|
+
const pending = channel === 'npm-global'
|
|
94
|
+
? findAutoUpdateConsent(sessionId, current, latest, channel)
|
|
95
|
+
: null;
|
|
96
|
+
const claim = readUpdatePromptClaim(sessionId, current, latest);
|
|
97
|
+
// The notice must have been shown to THIS session and not answered yet.
|
|
98
|
+
// Once answered, later "no"/"later" in ordinary conversation is not a
|
|
99
|
+
// decision about updates (it used to escalate the snooze every time).
|
|
100
|
+
const open = (claim && claim.decision !== 'answered') || pending?.decision === 'pending';
|
|
101
|
+
if (!open) return null;
|
|
102
|
+
|
|
103
|
+
if (decision === 'approved') {
|
|
104
|
+
if (!pending || pending.decision !== 'pending') return null;
|
|
105
|
+
if (!writeAutoUpdateConsent(sessionId, current, latest, pending.channel ?? channel, 'approved')) return null;
|
|
106
|
+
markUpdatePromptAnswered(sessionId, current, latest, 'approved');
|
|
107
|
+
return 'approved';
|
|
108
|
+
}
|
|
109
|
+
// declined | never
|
|
110
|
+
try { writeSnooze(memeshDir(), latest); } catch { /* best-effort */ }
|
|
111
|
+
if (pending?.decision === 'pending') {
|
|
112
|
+
writeAutoUpdateConsent(sessionId, current, latest, pending.channel ?? channel, 'declined');
|
|
113
|
+
}
|
|
114
|
+
let recorded = decision;
|
|
115
|
+
if (decision === 'never') {
|
|
116
|
+
try {
|
|
117
|
+
const configMod = await importFromPluginRoot(resolvePluginRoot(import.meta.url), 'dist/core/config.js');
|
|
118
|
+
configMod.updateConfig({ updateCheck: false });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logError('user-prompt-intent', `could not persist updateCheck=false: ${err?.message || err}`);
|
|
121
|
+
recorded = 'declined';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
markUpdatePromptAnswered(sessionId, current, latest, recorded);
|
|
125
|
+
return recorded;
|
|
77
126
|
}
|
|
78
127
|
|
|
79
128
|
// Patterns compiled at module load — invalid regex MUST fail loudly. Do
|
|
@@ -169,12 +218,44 @@ function logError(scope, msg) {
|
|
|
169
218
|
// `file://${process.argv[1]}` produces an invalid URL because the path uses
|
|
170
219
|
// backslashes; pathToFileURL() correctly normalizes to a file:// URL on
|
|
171
220
|
// every platform, so the comparison is portable.
|
|
172
|
-
|
|
221
|
+
//
|
|
222
|
+
// `import.meta.url` is the REAL path of this file; `process.argv[1]` is the
|
|
223
|
+
// path as the host typed it. Through a symlink (plugin cache, global npm
|
|
224
|
+
// prefix) the two differ, so compare the typed path first — under
|
|
225
|
+
// `--preserve-symlinks-main` Node leaves `import.meta.url` unresolved too, and
|
|
226
|
+
// resolving the typed side unconditionally would make them diverge — and only
|
|
227
|
+
// then the resolved path. A realpath failure on that fallback is left to
|
|
228
|
+
// throw: a crash is visible, "not the entry point" is not.
|
|
229
|
+
//
|
|
230
|
+
// Also reads "not main" for `node -` (`argv[1]` is the literal `-`;
|
|
231
|
+
// `realpathSync('-')` would throw) and `node -e`/`-p` (the eval flag is in
|
|
232
|
+
// `process.execArgv`; `argv[1]` is just the caller's first argument). That
|
|
233
|
+
// is `isMain()`'s (scripts/lib/verify-core.mjs) `-`/eval handling and its
|
|
234
|
+
// typed-path-then-realpath order, without its directory case: `node <dir>`
|
|
235
|
+
// reads as "not main" here, where `isMain()` throws. The hook cannot import
|
|
236
|
+
// `isMain()`: it ships standalone (package.json `files`).
|
|
237
|
+
const EVAL_FLAG = /^(-e|-p|-pe|--eval|--print)(=|$)/;
|
|
238
|
+
const entryPath = process.argv[1];
|
|
239
|
+
const isMainModule = Boolean(entryPath)
|
|
240
|
+
&& entryPath !== '-'
|
|
241
|
+
&& !process.execArgv.some((arg) => EVAL_FLAG.test(arg))
|
|
242
|
+
&& (import.meta.url === pathToFileURL(entryPath).href
|
|
243
|
+
|| import.meta.url === pathToFileURL(realpathSync(entryPath)).href);
|
|
173
244
|
if (isMainModule) {
|
|
245
|
+
// See post-commit.js for why every exit path leaves a record (#327). This
|
|
246
|
+
// hook's only effect is the additionalContext it injects, so its outcome is
|
|
247
|
+
// `notified`, not `wrote`: doctor's `writes` answers "is memory capture
|
|
248
|
+
// still alive", and an injected hint is not a memory. Both branches that
|
|
249
|
+
// reach it — a remember intent and an update-consent decision — inject and
|
|
250
|
+
// store nothing, so there is no writing branch here to keep.
|
|
251
|
+
let payload = null;
|
|
252
|
+
const record = (outcome, reason, entity) =>
|
|
253
|
+
recordHookOutcome(process.env, { hook: 'user-prompt-intent', outcome, reason, entity, payload });
|
|
254
|
+
|
|
174
255
|
let input = '';
|
|
175
256
|
process.stdin.setEncoding('utf8');
|
|
176
257
|
process.stdin.on('data', (chunk) => { input += chunk; });
|
|
177
|
-
process.stdin.on('end', () => {
|
|
258
|
+
process.stdin.on('end', async () => {
|
|
178
259
|
try {
|
|
179
260
|
// Distinguish empty stdin (legitimate degenerate event) from malformed
|
|
180
261
|
// input (protocol drift). Both stay non-blocking, but only malformed
|
|
@@ -186,6 +267,7 @@ if (isMainModule) {
|
|
|
186
267
|
data = JSON.parse(trimmed);
|
|
187
268
|
} catch (parseErr) {
|
|
188
269
|
logError('user-prompt-intent', `malformed stdin JSON (len=${input.length}): ${parseErr.message}`);
|
|
270
|
+
record('error', 'malformed stdin JSON');
|
|
189
271
|
return process.exit(0);
|
|
190
272
|
}
|
|
191
273
|
}
|
|
@@ -194,26 +276,46 @@ if (isMainModule) {
|
|
|
194
276
|
// Claude Code's transcript format changed once before (2026-05-07), so
|
|
195
277
|
// we accept either name to survive a similar rename. If both are absent
|
|
196
278
|
// or non-string, detectRememberIntent's type guard returns false safely.
|
|
279
|
+
payload = data;
|
|
197
280
|
const prompt = data.prompt ?? data.user_prompt ?? '';
|
|
198
|
-
const updateDecision = recordUpdateConsent(data.session_id, prompt);
|
|
281
|
+
const updateDecision = await recordUpdateConsent(data.session_id, prompt);
|
|
199
282
|
const rememberIntent = detectRememberIntent(prompt);
|
|
200
|
-
|
|
283
|
+
// Messages waiting for the recipient this session declared in
|
|
284
|
+
// MEMESH_RECIPIENT. Read-only and not memory capture, so it is not
|
|
285
|
+
// gated by autoCapture. Empty when the variable is unset. An inbox that
|
|
286
|
+
// cannot be read is recorded as an `error` of its own (a label, never
|
|
287
|
+
// the message), next to whatever this prompt's outcome turns out to be,
|
|
288
|
+
// so a skip below cannot be read as "nothing was waiting".
|
|
289
|
+
const inboxLines = unreadMessageLines(process.env, (err) => record('error', `inbox: ${hookErrorReason(err)}`));
|
|
290
|
+
if (!rememberIntent && !updateDecision && inboxLines.length === 0) {
|
|
291
|
+
record('skipped', SKIP_REASONS.noPromptIntent);
|
|
292
|
+
return process.exit(0);
|
|
293
|
+
}
|
|
201
294
|
// Update consent is a user-authorized control decision, not memory
|
|
202
295
|
// capture; it must still be recorded when auto-capture is disabled.
|
|
203
|
-
if (!isAutoCaptureEnabled(process.env) && !updateDecision
|
|
296
|
+
if (!isAutoCaptureEnabled(process.env) && !updateDecision && inboxLines.length === 0) {
|
|
297
|
+
record('skipped', SKIP_REASONS.autoCaptureOff);
|
|
298
|
+
return process.exit(0);
|
|
299
|
+
}
|
|
204
300
|
|
|
205
301
|
const contexts = [];
|
|
206
|
-
if (updateDecision) {
|
|
207
|
-
contexts.push(
|
|
208
|
-
|
|
209
|
-
|
|
302
|
+
if (updateDecision === 'approved') {
|
|
303
|
+
contexts.push('The user explicitly approved the MeMesh upgrade. The Stop hook may now update the consented installation.');
|
|
304
|
+
} else if (updateDecision === 'declined') {
|
|
305
|
+
contexts.push('The user declined the MeMesh upgrade. It is snoozed for this target version (24h, then 48h, then 7 days on repeated declines); do not install it or mention it again unless a newer version appears.');
|
|
306
|
+
} else if (updateDecision === 'never') {
|
|
307
|
+
contexts.push('The user asked never to be asked about MeMesh updates again. updateCheck is now off; do not mention updates. `memesh config set updateCheck true` turns checks back on.');
|
|
210
308
|
}
|
|
211
|
-
|
|
309
|
+
const hint = rememberIntent && isAutoCaptureEnabled(process.env);
|
|
310
|
+
if (hint) contexts.push(buildHint());
|
|
311
|
+
if (inboxLines.length > 0) contexts.push(inboxLines.join('\n'));
|
|
212
312
|
const out = { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: contexts.join('\n\n') } };
|
|
213
313
|
process.stdout.write(JSON.stringify(out));
|
|
314
|
+
record('notified', undefined, `hint:${updateDecision ?? (hint ? 'remember-intent' : 'inbox')}`);
|
|
214
315
|
process.exit(0);
|
|
215
316
|
} catch (err) {
|
|
216
317
|
logError('user-prompt-intent', err?.message || err);
|
|
318
|
+
record('error', hookErrorReason(err));
|
|
217
319
|
process.exit(0);
|
|
218
320
|
}
|
|
219
321
|
});
|
|
@@ -497,14 +497,13 @@ fi
|
|
|
497
497
|
# that never contained them. `git archive` extracts exactly the tree at
|
|
498
498
|
# $MARKETPLACE_SHA; nothing untracked or uncommitted can reach the cache.
|
|
499
499
|
# node_modules and .git are never tracked, so they never appear in the
|
|
500
|
-
# archive; tests/benchmarks
|
|
501
|
-
#
|
|
502
|
-
# before.
|
|
500
|
+
# archive; tests/benchmarks are tracked (real source) and are removed after
|
|
501
|
+
# extraction to keep the shipped cache the same shape as before.
|
|
503
502
|
git -C "$MARKETPLACE_DIR" archive "$MARKETPLACE_SHA" | tar -x -C "$STAGE_PATH" || {
|
|
504
503
|
echo "ERROR: git archive failed — the live cache at $NEW_INSTALL_PATH was not touched" >&2
|
|
505
504
|
exit 1
|
|
506
505
|
}
|
|
507
|
-
if ! rm -rf "$STAGE_PATH/tests" "$STAGE_PATH/benchmarks"
|
|
506
|
+
if ! rm -rf "$STAGE_PATH/tests" "$STAGE_PATH/benchmarks"; then
|
|
508
507
|
echo "ERROR: could not remove development-only files from the staging copy — the live cache was not touched." >&2
|
|
509
508
|
exit 1
|
|
510
509
|
fi
|