@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.
Files changed (159) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/AGENTS.md +75 -13
  5. package/README.de.md +5 -5
  6. package/README.md +5 -5
  7. package/README.zh-TW.md +5 -5
  8. package/dashboard/dist/index.html +8 -8
  9. package/dist/cli/view-live.d.ts.map +1 -1
  10. package/dist/cli/view-live.js +0 -427
  11. package/dist/cli/view-live.js.map +1 -1
  12. package/dist/core/agent-message-inbox.d.ts +6 -0
  13. package/dist/core/agent-message-inbox.d.ts.map +1 -1
  14. package/dist/core/agent-message-inbox.js +28 -1
  15. package/dist/core/agent-message-inbox.js.map +1 -1
  16. package/dist/core/briefing-index.d.ts +30 -0
  17. package/dist/core/briefing-index.d.ts.map +1 -0
  18. package/dist/core/briefing-index.js +140 -0
  19. package/dist/core/briefing-index.js.map +1 -0
  20. package/dist/core/briefing-level.d.ts +22 -0
  21. package/dist/core/briefing-level.d.ts.map +1 -0
  22. package/dist/core/briefing-level.js +75 -0
  23. package/dist/core/briefing-level.js.map +1 -0
  24. package/dist/core/briefing.d.ts +7 -0
  25. package/dist/core/briefing.d.ts.map +1 -1
  26. package/dist/core/briefing.js +72 -12
  27. package/dist/core/briefing.js.map +1 -1
  28. package/dist/core/capture-liveness.d.ts +137 -0
  29. package/dist/core/capture-liveness.d.ts.map +1 -0
  30. package/dist/core/capture-liveness.js +448 -0
  31. package/dist/core/capture-liveness.js.map +1 -0
  32. package/dist/core/config.d.ts +2 -0
  33. package/dist/core/config.d.ts.map +1 -1
  34. package/dist/core/config.js +5 -1
  35. package/dist/core/config.js.map +1 -1
  36. package/dist/core/delegation.d.ts +47 -0
  37. package/dist/core/delegation.d.ts.map +1 -0
  38. package/dist/core/delegation.js +174 -0
  39. package/dist/core/delegation.js.map +1 -0
  40. package/dist/core/doctor-fixes.d.ts +1 -0
  41. package/dist/core/doctor-fixes.d.ts.map +1 -1
  42. package/dist/core/doctor-fixes.js +21 -0
  43. package/dist/core/doctor-fixes.js.map +1 -1
  44. package/dist/core/doctor.d.ts +8 -0
  45. package/dist/core/doctor.d.ts.map +1 -1
  46. package/dist/core/doctor.js +148 -3
  47. package/dist/core/doctor.js.map +1 -1
  48. package/dist/core/note-derive.d.ts +14 -0
  49. package/dist/core/note-derive.d.ts.map +1 -0
  50. package/dist/core/note-derive.js +66 -0
  51. package/dist/core/note-derive.js.map +1 -0
  52. package/dist/core/note-ingest.d.ts +34 -0
  53. package/dist/core/note-ingest.d.ts.map +1 -0
  54. package/dist/core/note-ingest.js +445 -0
  55. package/dist/core/note-ingest.js.map +1 -0
  56. package/dist/core/operations.d.ts +10 -1
  57. package/dist/core/operations.d.ts.map +1 -1
  58. package/dist/core/operations.js +129 -14
  59. package/dist/core/operations.js.map +1 -1
  60. package/dist/core/paths.d.ts +1 -0
  61. package/dist/core/paths.d.ts.map +1 -1
  62. package/dist/core/paths.js +4 -0
  63. package/dist/core/paths.js.map +1 -1
  64. package/dist/core/schema-export.d.ts.map +1 -1
  65. package/dist/core/schema-export.js +15 -5
  66. package/dist/core/schema-export.js.map +1 -1
  67. package/dist/core/serializer.d.ts +2 -0
  68. package/dist/core/serializer.d.ts.map +1 -1
  69. package/dist/core/serializer.js +119 -3
  70. package/dist/core/serializer.js.map +1 -1
  71. package/dist/core/session-insight.d.ts +44 -0
  72. package/dist/core/session-insight.d.ts.map +1 -0
  73. package/dist/core/session-insight.js +216 -0
  74. package/dist/core/session-insight.js.map +1 -0
  75. package/dist/core/task-state-store.d.ts +4 -0
  76. package/dist/core/task-state-store.d.ts.map +1 -1
  77. package/dist/core/task-state-store.js +18 -5
  78. package/dist/core/task-state-store.js.map +1 -1
  79. package/dist/core/task-state.d.ts +5 -0
  80. package/dist/core/task-state.d.ts.map +1 -1
  81. package/dist/core/task-state.js +70 -0
  82. package/dist/core/task-state.js.map +1 -1
  83. package/dist/core/turn-signal.d.ts +22 -0
  84. package/dist/core/turn-signal.d.ts.map +1 -0
  85. package/dist/core/turn-signal.js +89 -0
  86. package/dist/core/turn-signal.js.map +1 -0
  87. package/dist/core/types.d.ts +14 -3
  88. package/dist/core/types.d.ts.map +1 -1
  89. package/dist/core/update-entrypoint.d.ts +19 -0
  90. package/dist/core/update-entrypoint.d.ts.map +1 -0
  91. package/dist/core/update-entrypoint.js +222 -0
  92. package/dist/core/update-entrypoint.js.map +1 -0
  93. package/dist/core/update-notice.d.ts +69 -0
  94. package/dist/core/update-notice.d.ts.map +1 -0
  95. package/dist/core/update-notice.js +188 -0
  96. package/dist/core/update-notice.js.map +1 -0
  97. package/dist/core/work-topology.d.ts +1 -0
  98. package/dist/core/work-topology.d.ts.map +1 -1
  99. package/dist/core/work-topology.js +3 -0
  100. package/dist/core/work-topology.js.map +1 -1
  101. package/dist/host-runtime/codex-session.d.ts +10 -0
  102. package/dist/host-runtime/codex-session.d.ts.map +1 -1
  103. package/dist/host-runtime/codex-session.js +1 -1
  104. package/dist/host-runtime/codex-session.js.map +1 -1
  105. package/dist/knowledge-graph.d.ts.map +1 -1
  106. package/dist/knowledge-graph.js +24 -3
  107. package/dist/knowledge-graph.js.map +1 -1
  108. package/dist/mcp/THIRD_PARTY_NOTICES.txt +2 -2
  109. package/dist/mcp/server.js +1438 -122
  110. package/dist/mcp/server.js.map +3 -3
  111. package/dist/skills-manifest.json +61 -36
  112. package/dist/storage/fts-index.d.ts +1 -0
  113. package/dist/storage/fts-index.d.ts.map +1 -1
  114. package/dist/storage/fts-index.js +5 -0
  115. package/dist/storage/fts-index.js.map +1 -1
  116. package/dist/transports/cli/cli.d.ts.map +1 -1
  117. package/dist/transports/cli/cli.js +7846 -5613
  118. package/dist/transports/cli/cli.js.map +3 -3
  119. package/dist/transports/http/server.d.ts.map +1 -1
  120. package/dist/transports/http/server.js +48 -28
  121. package/dist/transports/http/server.js.map +1 -1
  122. package/dist/transports/mcp/handlers.d.ts +31 -7
  123. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  124. package/dist/transports/mcp/handlers.js +49 -7
  125. package/dist/transports/mcp/handlers.js.map +1 -1
  126. package/dist/transports/schemas.d.ts +5 -2
  127. package/dist/transports/schemas.d.ts.map +1 -1
  128. package/dist/transports/schemas.js +30 -3
  129. package/dist/transports/schemas.js.map +1 -1
  130. package/docs/platforms/README.md +1 -0
  131. package/docs/platforms/agent-messaging.md +39 -8
  132. package/package.json +11 -6
  133. package/scripts/hooks/_generated/agent-message-inbox.js +28 -1
  134. package/scripts/hooks/_generated/briefing-index.js +147 -0
  135. package/scripts/hooks/_generated/briefing-level.js +82 -0
  136. package/scripts/hooks/_generated/capture-liveness.js +455 -0
  137. package/scripts/hooks/_generated/core-paths.js +4 -0
  138. package/scripts/hooks/_generated/fts-index.js +5 -0
  139. package/scripts/hooks/_generated/task-state.js +70 -0
  140. package/scripts/hooks/_generated/update-notice.js +195 -0
  141. package/scripts/hooks/_generated/work-topology.js +3 -0
  142. package/scripts/hooks/_shared.js +923 -27
  143. package/scripts/hooks/_stop-notes.js +504 -0
  144. package/scripts/hooks/auto-update-runner.mjs +22 -0
  145. package/scripts/hooks/decision-nudge.js +32 -5
  146. package/scripts/hooks/guard-check.js +30 -3
  147. package/scripts/hooks/post-commit.js +329 -133
  148. package/scripts/hooks/pre-compact.js +17 -2
  149. package/scripts/hooks/pre-edit-recall.js +416 -64
  150. package/scripts/hooks/session-start.js +617 -150
  151. package/scripts/hooks/session-summary.js +217 -41
  152. package/scripts/hooks/user-prompt-intent.js +124 -22
  153. package/scripts/upgrade-plugin.sh +3 -4
  154. package/skills/memesh/SKILL.md +45 -23
  155. package/dist/cli/assets/d3.v7.min.js +0 -2
  156. package/dist/core/graph.d.ts +0 -26
  157. package/dist/core/graph.d.ts.map +0 -1
  158. package/dist/core/graph.js +0 -74
  159. package/dist/core/graph.js.map +0 -1
@@ -0,0 +1,82 @@
1
+ // ============================================================================
2
+ // AUTO-GENERATED from src/core/briefing-level.ts — DO NOT EDIT BY HAND.
3
+ // Regenerate with: npm run build (scripts/generate-hook-core.mjs)
4
+ //
5
+ // Claude Code hooks import this committed copy instead of dist/, so the
6
+ // always-on capture path survives a missing or stale dist/ while staying
7
+ // byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
8
+ // ============================================================================
9
+ export const BRIEFING_LEVELS = ['minimal', 'standard', 'full'];
10
+ export const DEFAULT_BRIEFING_LEVEL = 'standard';
11
+ export function isBriefingLevel(value) {
12
+ return typeof value === 'string' && BRIEFING_LEVELS.includes(value);
13
+ }
14
+ const INVALID_VALUE_SERIALIZED_MAX = 100;
15
+ const PRE_SLICE_RAW_MAX = 256;
16
+ function safeRawPreSlice(value, maxUnits) {
17
+ if (value.length <= maxUnits)
18
+ return value;
19
+ let end = maxUnits;
20
+ const lastKept = value.charCodeAt(end - 1);
21
+ const nextUnit = value.charCodeAt(end);
22
+ const lastKeptIsHighSurrogate = lastKept >= 0xd800 && lastKept <= 0xdbff;
23
+ const nextUnitIsLowSurrogate = nextUnit >= 0xdc00 && nextUnit <= 0xdfff;
24
+ if (lastKeptIsHighSurrogate && nextUnitIsLowSurrogate)
25
+ end -= 1;
26
+ return value.slice(0, end);
27
+ }
28
+ function truncateToSerializedBound(codePoints) {
29
+ let kept = 0;
30
+ for (; kept < codePoints.length; kept++) {
31
+ const candidate = JSON.stringify(`${codePoints.slice(0, kept + 1).join('')}…`);
32
+ if (candidate.length > INVALID_VALUE_SERIALIZED_MAX)
33
+ break;
34
+ }
35
+ return JSON.stringify(`${codePoints.slice(0, kept).join('')}…`);
36
+ }
37
+ function describeInvalidValue(value) {
38
+ if (typeof value === 'string') {
39
+ if (value.length <= INVALID_VALUE_SERIALIZED_MAX) {
40
+ const whole = JSON.stringify(value);
41
+ if (whole.length <= INVALID_VALUE_SERIALIZED_MAX)
42
+ return whole;
43
+ return truncateToSerializedBound(Array.from(value));
44
+ }
45
+ return truncateToSerializedBound(Array.from(safeRawPreSlice(value, PRE_SLICE_RAW_MAX)));
46
+ }
47
+ if (Array.isArray(value))
48
+ return '[array]';
49
+ if (value !== null && typeof value === 'object')
50
+ return '[object]';
51
+ if (typeof value === 'bigint')
52
+ return '[bigint]';
53
+ if (typeof value === 'symbol')
54
+ return '[symbol]';
55
+ if (typeof value === 'function')
56
+ return '[function]';
57
+ return JSON.stringify(value) ?? String(value);
58
+ }
59
+ export function resolveBriefingLevel(envValue, configValue) {
60
+ if (envValue !== undefined) {
61
+ if (isBriefingLevel(envValue))
62
+ return { level: envValue, invalid: null };
63
+ return { level: DEFAULT_BRIEFING_LEVEL, invalid: { source: 'env', value: describeInvalidValue(envValue) } };
64
+ }
65
+ if (configValue !== undefined) {
66
+ if (isBriefingLevel(configValue))
67
+ return { level: configValue, invalid: null };
68
+ return { level: DEFAULT_BRIEFING_LEVEL, invalid: { source: 'config', value: describeInvalidValue(configValue) } };
69
+ }
70
+ return { level: DEFAULT_BRIEFING_LEVEL, invalid: null };
71
+ }
72
+ const POLICIES = {
73
+ minimal: { global: false, foreign: false, taskState: false, index: false, workPackageNotice: false },
74
+ standard: { global: false, foreign: false, taskState: true, index: true, workPackageNotice: false },
75
+ full: { global: true, foreign: true, taskState: true, index: true, workPackageNotice: true },
76
+ };
77
+ export function briefingLevelPolicy(level) {
78
+ return POLICIES[level];
79
+ }
80
+ export function sessionStartAppendsWorkPackageNotice(level) {
81
+ return briefingLevelPolicy(level).workPackageNotice;
82
+ }
@@ -0,0 +1,455 @@
1
+ // ============================================================================
2
+ // AUTO-GENERATED from src/core/capture-liveness.ts — DO NOT EDIT BY HAND.
3
+ // Regenerate with: npm run build (scripts/generate-hook-core.mjs)
4
+ //
5
+ // Claude Code hooks import this committed copy instead of dist/, so the
6
+ // always-on capture path survives a missing or stale dist/ while staying
7
+ // byte-locked to core — eliminating the hand-mirror drift behind the P0 FTS bug.
8
+ // ============================================================================
9
+ export const HOOK_OUTCOMES_FILENAME = 'hook-outcomes.jsonl';
10
+ export const HOOK_OUTCOMES_PER_HOOK = 20;
11
+ export const HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK = 5;
12
+ function windowKeep(entries, maxTriggered, maxNotTriggered) {
13
+ const keep = new Array(entries.length).fill(false);
14
+ const seen = new Map();
15
+ for (let i = entries.length - 1; i >= 0; i--) {
16
+ const { hook, triggered } = entries[i];
17
+ const counts = seen.get(hook) ?? { t: 0, n: 0 };
18
+ seen.set(hook, counts);
19
+ if (triggered) {
20
+ if (++counts.t <= maxTriggered)
21
+ keep[i] = true;
22
+ }
23
+ else if (++counts.n <= maxNotTriggered) {
24
+ keep[i] = true;
25
+ }
26
+ }
27
+ return keep;
28
+ }
29
+ export function isTriggeredRecord(record) {
30
+ if (record.outcome !== 'skipped' || record.reason === undefined)
31
+ return true;
32
+ return !(NOT_TRIGGERED_SKIP_REASONS[record.hook] ?? []).includes(record.reason);
33
+ }
34
+ function readHookOutcomeLines(raw) {
35
+ const entries = [];
36
+ for (const line of raw.split('\n')) {
37
+ const record = parseHookOutcomeLine(line);
38
+ if (record)
39
+ entries.push({ hook: record.hook, triggered: isTriggeredRecord(record), record, line });
40
+ }
41
+ return entries;
42
+ }
43
+ export const HOOK_OUTCOMES_ROTATE_BYTES = 64 * 1024;
44
+ export function serializeHookOutcome(record) {
45
+ return `${JSON.stringify(record)}\n`;
46
+ }
47
+ export function trimHookOutcomeLines(raw, max = HOOK_OUTCOMES_PER_HOOK, maxBytes = HOOK_OUTCOMES_ROTATE_BYTES) {
48
+ const records = readHookOutcomeLines(raw);
49
+ const keep = windowKeep(records, max, HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK);
50
+ let kept = records.filter((_, i) => keep[i]).map((r) => r.line);
51
+ let bytes = kept.reduce((n, line) => n + utf8Length(line) + 1, 0);
52
+ if (bytes > maxBytes) {
53
+ const budget = maxBytes / 2;
54
+ const fit = [];
55
+ bytes = 0;
56
+ for (let i = kept.length - 1; i >= 0; i--) {
57
+ const size = utf8Length(kept[i]) + 1;
58
+ if (size > budget)
59
+ continue;
60
+ if (bytes + size > budget)
61
+ break;
62
+ fit.push(kept[i]);
63
+ bytes += size;
64
+ }
65
+ kept = fit.reverse();
66
+ }
67
+ return kept.length ? `${kept.join('\n')}\n` : '';
68
+ }
69
+ function utf8Length(text) {
70
+ let n = 0;
71
+ for (let i = 0; i < text.length; i++) {
72
+ const c = text.charCodeAt(i);
73
+ if (c < 0x80)
74
+ n += 1;
75
+ else if (c < 0x800)
76
+ n += 2;
77
+ else if (c >= 0xd800 && c <= 0xdbff) {
78
+ n += 4;
79
+ i++;
80
+ }
81
+ else
82
+ n += 3;
83
+ }
84
+ return n;
85
+ }
86
+ export const SILENT_HOOK_MIN_RUNS = 5;
87
+ export const CAPTURE_HOOKS = [
88
+ 'post-commit',
89
+ 'session-summary',
90
+ 'pre-compact',
91
+ 'pre-edit-recall',
92
+ 'user-prompt-intent',
93
+ 'decision-nudge',
94
+ 'guard-check',
95
+ 'session-start',
96
+ 'note-ingest',
97
+ 'remember-nudge',
98
+ ];
99
+ export const FAIL_ELIGIBLE_HOOKS = ['session-summary'];
100
+ export const SILENT_ELIGIBLE_HOOKS = ['post-commit', 'session-summary', 'pre-compact'];
101
+ export const SKIP_REASONS = {
102
+ notBash: 'not a Bash tool call',
103
+ notGitCommit: 'not a git commit command',
104
+ commitLineMissing: 'a git commit ran but printed no commit line',
105
+ commitHeadBaseline: 'recorded the repository HEAD baseline; no history was backfilled',
106
+ commitHeadUnchanged: 'a commit-like command completed but repository HEAD did not change',
107
+ commitHeadUnresolvable: 'a commit-like command ran but repository HEAD could not be resolved',
108
+ alreadyCaptured: 'this session was already captured',
109
+ payloadTooLarge: 'payload exceeded the stdin byte cap',
110
+ toolNameAbsent: 'tool_name absent in payload',
111
+ notDecisionTool: 'not a decision-shaped tool call',
112
+ noSessionId: 'no usable session_id in the payload',
113
+ alreadyNudged: 'already nudged for this tool in this session',
114
+ noBashCommand: 'no Bash command in the payload',
115
+ noDatabaseForGuards: 'no database yet — nothing to guard against',
116
+ noGuardMatched: 'no active guard matched this command',
117
+ autoCaptureOff: 'auto-capture is turned off',
118
+ commitCwdAbsent: 'data.cwd absent — cannot resolve project or repo',
119
+ hashNotACommit: 'the hash is not a commit in this repository',
120
+ noSessionOrTranscript: 'neither session_id nor transcript_path in the payload',
121
+ emptyStdin: 'empty stdin',
122
+ cwdAbsent: 'cwd absent in payload — cannot resolve project',
123
+ notAgenticLoop: 'not an agentic loop',
124
+ transcriptPathAbsent: 'transcript_path absent',
125
+ transcriptGone: 'the transcript file named by the payload is gone',
126
+ tooLittleActivity: 'too little activity in the session to be worth saving',
127
+ noRuleMatched: 'no rule matched (no edited file and fewer than 20 tool calls)',
128
+ allMatchedEntitiesArchived: 'every rule that matched targeted an entity the user forget-archived',
129
+ toolInputAbsent: 'tool_input absent in payload',
130
+ noFilePath: 'no file_path in the tool input',
131
+ noDatabaseForRecall: 'no database yet — nothing to recall',
132
+ nothingToRecall: 'no guard matched and nothing to recall for this file',
133
+ candidateWindowTruncated: 'more candidates may exist than the search window examined',
134
+ noPromptIntent: 'the prompt carried no remember intent and no update decision',
135
+ noMemoryDir: 'no Claude Code memory directory for this project',
136
+ noNoteChanged: 'no note file changed since the last ingestion',
137
+ noteIngesterNotBuilt: 'the note ingester is not built (dist/core/note-ingest.js is missing)',
138
+ noteNothingNew: 'note files were read and nothing new needed storing',
139
+ noteFilesRefused: 'note files were refused and nothing was stored',
140
+ noTranscript: 'no transcript to read',
141
+ trivialTurn: 'trivial turn — too few tool calls since the last Stop',
142
+ noDecisionMove: 'no decision-shaped move since the last Stop',
143
+ memoryWritten: 'a memory was written since the last Stop',
144
+ noteFileChanged: 'a note file changed since the last Stop',
145
+ };
146
+ const KNOWN_SKIP_REASONS = new Set(Object.values(SKIP_REASONS));
147
+ export const UNRECOGNISED_REASON = 'unrecognised reason';
148
+ export function renderableSkipReason(reason) {
149
+ if (reason === undefined)
150
+ return 'unspecified';
151
+ return KNOWN_SKIP_REASONS.has(reason) ? reason : UNRECOGNISED_REASON;
152
+ }
153
+ export function isGitCommitCommand(command) {
154
+ const start = /(?:^|[\s;&|(`/])git(?=\s)/g;
155
+ let m;
156
+ while ((m = start.exec(command)) !== null) {
157
+ const walk = commitFollowsGit(command, m.index + m[0].length);
158
+ if (walk.commit)
159
+ return true;
160
+ if (walk.end > start.lastIndex)
161
+ start.lastIndex = walk.end;
162
+ }
163
+ return false;
164
+ }
165
+ const VALUE_OPTIONS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace']);
166
+ const COMMIT_END = /[\s;&|)`]/;
167
+ const COMMIT_SUBCOMMANDS = ['commit', 'merge', 'cherry-pick', 'revert'];
168
+ function commitFollowsGit(s, i) {
169
+ for (;;) {
170
+ const afterSpace = skipSpace(s, i);
171
+ if (afterSpace === i)
172
+ return { commit: false, end: i };
173
+ i = afterSpace;
174
+ const subcommand = COMMIT_SUBCOMMANDS.find((candidate) => s.startsWith(candidate, i));
175
+ if (subcommand) {
176
+ const next = s[i + subcommand.length];
177
+ return { commit: next === undefined || COMMIT_END.test(next), end: i + subcommand.length };
178
+ }
179
+ if (s[i] !== '-')
180
+ return { commit: false, end: i };
181
+ const optionEnd = tokenEnd(s, i);
182
+ const option = s.slice(i, optionEnd);
183
+ i = optionEnd;
184
+ if (VALUE_OPTIONS.has(option)) {
185
+ const valueStart = skipSpace(s, i);
186
+ if (valueStart === i || valueStart >= s.length)
187
+ return { commit: false, end: valueStart };
188
+ i = valueEnd(s, valueStart);
189
+ }
190
+ }
191
+ }
192
+ function skipSpace(s, i) {
193
+ while (i < s.length && /\s/.test(s[i]))
194
+ i++;
195
+ return i;
196
+ }
197
+ function tokenEnd(s, i) {
198
+ while (i < s.length && !/\s/.test(s[i]))
199
+ i++;
200
+ return i;
201
+ }
202
+ function valueEnd(s, i) {
203
+ const quote = s[i];
204
+ if (quote === '"' || quote === "'") {
205
+ const close = s.indexOf(quote, i + 1);
206
+ return close === -1 ? s.length : close + 1;
207
+ }
208
+ return tokenEnd(s, i);
209
+ }
210
+ export const NOT_TRIGGERED_SKIP_REASONS = {
211
+ 'post-commit': [SKIP_REASONS.notBash, SKIP_REASONS.notGitCommit],
212
+ 'session-summary': [SKIP_REASONS.alreadyCaptured],
213
+ 'note-ingest': [SKIP_REASONS.noNoteChanged],
214
+ 'remember-nudge': [SKIP_REASONS.trivialTurn, SKIP_REASONS.noDecisionMove],
215
+ };
216
+ export const UNCLASSIFIED_SKIP_HOOKS = [
217
+ 'pre-compact',
218
+ 'pre-edit-recall',
219
+ 'user-prompt-intent',
220
+ 'decision-nudge',
221
+ 'guard-check',
222
+ 'session-start',
223
+ ];
224
+ export const NEVER_RAN_GRACE_HOURS = 72;
225
+ export function parseHookOutcomes(raw, limit = HOOK_OUTCOMES_PER_HOOK) {
226
+ if (!raw)
227
+ return { hooks: {} };
228
+ const entries = readHookOutcomeLines(raw);
229
+ const keep = windowKeep(entries, limit, HOOK_OUTCOMES_NOT_TRIGGERED_PER_HOOK);
230
+ const hooks = {};
231
+ entries.forEach(({ hook, record }, i) => {
232
+ if (!keep[i])
233
+ return;
234
+ (hooks[hook] ?? (hooks[hook] = [])).push(record);
235
+ });
236
+ return { hooks };
237
+ }
238
+ export function parseHookOutcomeLine(line) {
239
+ const trimmed = line.trim();
240
+ if (!trimmed)
241
+ return null;
242
+ let parsed;
243
+ try {
244
+ parsed = JSON.parse(trimmed);
245
+ }
246
+ catch {
247
+ return null;
248
+ }
249
+ if (!parsed || typeof parsed !== 'object')
250
+ return null;
251
+ const rec = parsed;
252
+ if (typeof rec.hook !== 'string' || !CAPTURE_HOOKS.includes(rec.hook))
253
+ return null;
254
+ if (typeof rec.at !== 'string')
255
+ return null;
256
+ if (rec.outcome !== 'wrote' && rec.outcome !== 'skipped'
257
+ && rec.outcome !== 'notified' && rec.outcome !== 'error')
258
+ return null;
259
+ const record = {
260
+ hook: rec.hook,
261
+ at: rec.at,
262
+ host: rec.host === 'claude-code' || rec.host === 'codex' ? rec.host : 'unknown',
263
+ outcome: rec.outcome,
264
+ };
265
+ const reason = typeof rec.reason === 'string' ? sanitizeRecordText(rec.reason) : '';
266
+ if (reason)
267
+ record.reason = reason;
268
+ const entity = typeof rec.entity === 'string' ? sanitizeRecordText(rec.entity) : '';
269
+ if (entity)
270
+ record.entity = entity;
271
+ return record;
272
+ }
273
+ export const RECORD_TEXT_MAX = 200;
274
+ export function sanitizeRecordText(text) {
275
+ return text.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, RECORD_TEXT_MAX);
276
+ }
277
+ export function summarizeHookOutcomes(file) {
278
+ const order = [...CAPTURE_HOOKS];
279
+ const names = Object.keys(file.hooks).sort((a, b) => {
280
+ const ai = order.indexOf(a);
281
+ const bi = order.indexOf(b);
282
+ if (ai !== bi)
283
+ return (ai === -1 ? order.length : ai) - (bi === -1 ? order.length : bi);
284
+ return a.localeCompare(b);
285
+ });
286
+ return names.map((hook) => summarizeOne(hook, file.hooks[hook] ?? []));
287
+ }
288
+ function summarizeOne(hook, records) {
289
+ let writes = 0;
290
+ let skips = 0;
291
+ let errors = 0;
292
+ let lastRunAt = null;
293
+ let firstTriggeredAt = null;
294
+ let triggeredRuns = 0;
295
+ let lastWriteAt = null;
296
+ let lastEntity = null;
297
+ let notifies = 0;
298
+ let lastNotifiedAt = null;
299
+ let lastSkipReason = null;
300
+ const skipCounts = new Map();
301
+ const hosts = new Set();
302
+ for (const r of records) {
303
+ hosts.add(r.host);
304
+ if (lastRunAt === null || r.at >= lastRunAt)
305
+ lastRunAt = r.at;
306
+ const triggered = isTriggeredRecord(r);
307
+ if (triggered) {
308
+ triggeredRuns++;
309
+ if (firstTriggeredAt === null || r.at < firstTriggeredAt)
310
+ firstTriggeredAt = r.at;
311
+ }
312
+ if (r.outcome === 'wrote') {
313
+ writes++;
314
+ if (lastWriteAt === null || r.at >= lastWriteAt) {
315
+ lastWriteAt = r.at;
316
+ lastEntity = r.entity ?? null;
317
+ }
318
+ }
319
+ else if (r.outcome === 'notified') {
320
+ notifies++;
321
+ if (lastNotifiedAt === null || r.at >= lastNotifiedAt)
322
+ lastNotifiedAt = r.at;
323
+ }
324
+ else if (r.outcome === 'skipped') {
325
+ skips++;
326
+ lastSkipReason = r.reason === undefined ? null : renderableSkipReason(r.reason);
327
+ if (triggered) {
328
+ const key = renderableSkipReason(r.reason);
329
+ skipCounts.set(key, (skipCounts.get(key) ?? 0) + 1);
330
+ }
331
+ }
332
+ else {
333
+ errors++;
334
+ }
335
+ }
336
+ let dominantSkipReason = null;
337
+ let dominantSkipCount = 0;
338
+ for (const [reason, count] of skipCounts) {
339
+ if (count > dominantSkipCount) {
340
+ dominantSkipCount = count;
341
+ dominantSkipReason = reason;
342
+ }
343
+ }
344
+ const runs = records.length;
345
+ return {
346
+ hook,
347
+ runs,
348
+ triggeredRuns,
349
+ writes,
350
+ skips,
351
+ errors,
352
+ lastRunAt,
353
+ firstTriggeredAt,
354
+ lastWriteAt,
355
+ lastEntity,
356
+ notifies,
357
+ lastNotifiedAt,
358
+ lastSkipReason,
359
+ dominantSkipReason,
360
+ dominantSkipCount,
361
+ hosts: [...hosts].sort(),
362
+ silent: SILENT_ELIGIBLE_HOOKS.includes(hook)
363
+ && triggeredRuns >= SILENT_HOOK_MIN_RUNS
364
+ && writes === 0,
365
+ };
366
+ }
367
+ export function summarizeTypeTrends(rows) {
368
+ return rows
369
+ .map((r) => ({ ...r, stopped: r.prev7 > 0 && r.last7 === 0 }))
370
+ .sort((a, b) => a.type.localeCompare(b.type));
371
+ }
372
+ export function captureLivenessVerdict(input) {
373
+ const withRecords = new Set(input.hooks.filter((h) => h.runs > 0).map((h) => h.hook));
374
+ const graceOver = input.measuringHours !== null &&
375
+ input.measuringHours !== undefined &&
376
+ input.measuringHours > NEVER_RAN_GRACE_HOURS;
377
+ const deadHooks = graceOver
378
+ ? (input.neverRanHooks ?? []).filter((h) => FAIL_ELIGIBLE_HOOKS.includes(h) && !withRecords.has(h)).sort()
379
+ : [];
380
+ const silent = input.hooks.filter((h) => h.silent).sort((a, b) => b.triggeredRuns - a.triggeredRuns);
381
+ const stoppedTypes = input.types.filter((t) => t.stopped);
382
+ let status = 'PASS';
383
+ if (deadHooks.length > 0)
384
+ status = 'FAIL';
385
+ else if (silent.length > 0 || stoppedTypes.length > 0)
386
+ status = 'PASS_WITH_CONCERNS';
387
+ return { status, silentHook: silent[0] ?? null, stoppedTypes, deadHooks };
388
+ }
389
+ export function captureLivenessNotice(verdict) {
390
+ if (verdict.status === 'PASS')
391
+ return null;
392
+ if (verdict.deadHooks.length > 0) {
393
+ const hook = verdict.deadHooks[0];
394
+ return `memesh: the ${hook} hook has never run — \`memesh doctor\` for the reason`;
395
+ }
396
+ const hook = verdict.silentHook;
397
+ if (hook) {
398
+ const since = (hook.firstTriggeredAt ?? '').slice(0, 10) || 'install';
399
+ return `memesh: ${hook.hook} ran ${hook.triggeredRuns} times since ${since} and wrote nothing — \`memesh doctor\` for the reason`;
400
+ }
401
+ const stopped = verdict.stoppedTypes[0];
402
+ if (stopped) {
403
+ return `memesh: nothing of type ${stopped.type} was captured this week (${stopped.prev7} last week) — \`memesh doctor\` for the reason`;
404
+ }
405
+ return null;
406
+ }
407
+ export const GRACE_SESSIONS = 3;
408
+ export const GRACE_HOURS = 24;
409
+ export function parseGraceState(raw) {
410
+ if (!raw)
411
+ return null;
412
+ let parsed;
413
+ try {
414
+ parsed = JSON.parse(raw);
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ if (!parsed || typeof parsed !== 'object')
420
+ return null;
421
+ const rec = parsed;
422
+ if (typeof rec.version !== 'string' || typeof rec.firstSeenAt !== 'string')
423
+ return null;
424
+ const sessions = typeof rec.sessions === 'number' && Number.isFinite(rec.sessions) ? rec.sessions : 0;
425
+ return { version: rec.version, firstSeenAt: rec.firstSeenAt, sessions };
426
+ }
427
+ export function advanceGraceState(previous, version, nowMs) {
428
+ if (!previous || previous.version !== version) {
429
+ return { version, firstSeenAt: new Date(nowMs).toISOString(), sessions: 1 };
430
+ }
431
+ return { ...previous, sessions: previous.sessions + 1 };
432
+ }
433
+ export function graceInEffect(state, nowMs) {
434
+ if (state.sessions <= GRACE_SESSIONS)
435
+ return true;
436
+ const startedMs = Date.parse(state.firstSeenAt);
437
+ if (!Number.isFinite(startedMs))
438
+ return false;
439
+ return nowMs - startedMs < GRACE_HOURS * 60 * 60 * 1000;
440
+ }
441
+ export function detectHookHost(payload, env = {}) {
442
+ if (env.MEMESH_HOOK_HOST === 'claude-code' || env.MEMESH_HOOK_HOST === 'codex') {
443
+ return env.MEMESH_HOOK_HOST;
444
+ }
445
+ if (env.CODEX_HOME || env.CODEX_SANDBOX || env.CODEX_PLUGIN_ROOT)
446
+ return 'codex';
447
+ if (env.CLAUDE_PLUGIN_ROOT || env.CLAUDE_PROJECT_DIR || env.CLAUDECODE)
448
+ return 'claude-code';
449
+ if (payload && typeof payload === 'object') {
450
+ if (typeof payload.transcript_path === 'string' || typeof payload.hook_event_name === 'string') {
451
+ return 'claude-code';
452
+ }
453
+ }
454
+ return 'unknown';
455
+ }
@@ -100,6 +100,10 @@ function tryGit(cwd, args) {
100
100
  return null;
101
101
  }
102
102
  }
103
+ export function gitRepoRoot(cwdInput) {
104
+ const cwd = cwdInput && cwdInput.length > 0 ? cwdInput : process.cwd();
105
+ return tryGit(cwd, ['rev-parse', '--show-toplevel']);
106
+ }
103
107
  export function canonicalRemoteLocator(remote) {
104
108
  const value = remote.trim();
105
109
  if (!value)
@@ -62,6 +62,11 @@ const LONE_UNSPACED_CHAR = new RegExp(`[${UNSPACED_SCRIPT_CLASS}]`, 'u');
62
62
  export function isLoneUnspacedChar(term) {
63
63
  return [...term].length === 1 && LONE_UNSPACED_CHAR.test(term);
64
64
  }
65
+ export function renderPhraseExpression(terms) {
66
+ if (terms.length === 0)
67
+ return null;
68
+ return `"${terms.map((term) => term.replace(/"/g, '""')).join(' ')}"`;
69
+ }
65
70
  function foldTitleIntoObservations(title, observationsText) {
66
71
  return title ? `${title} ${observationsText}` : observationsText;
67
72
  }
@@ -96,3 +96,73 @@ export function taskStateLines(state, project, now = new Date()) {
96
96
  }
97
97
  return lines;
98
98
  }
99
+ export const STALE_TASK_STATE_HOURS = 72;
100
+ export const CLOCK_SKEW_ALLOWANCE_MINUTES = 5;
101
+ const ZONED_INSTANT = /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})[Tt](?<hour>\d{2}):(?<minute>\d{2})(?::(?<second>\d{2})(?:\.\d+)?)?(?:(?<zulu>[Zz])|(?<offSign>[+-])(?<offHour>\d{2}):?(?<offMinute>\d{2}))$/;
102
+ function isLeapYear(year) {
103
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
104
+ }
105
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
106
+ function daysInMonth(year, month) {
107
+ return month === 2 && isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1];
108
+ }
109
+ function isRealInstant(groups) {
110
+ const year = Number(groups.year);
111
+ const month = Number(groups.month);
112
+ const day = Number(groups.day);
113
+ const hour = Number(groups.hour);
114
+ const minute = Number(groups.minute);
115
+ const second = groups.second === undefined ? 0 : Number(groups.second);
116
+ if (month < 1 || month > 12)
117
+ return false;
118
+ if (day < 1 || day > daysInMonth(year, month))
119
+ return false;
120
+ if (hour > 23)
121
+ return false;
122
+ if (minute > 59)
123
+ return false;
124
+ if (second > 59)
125
+ return false;
126
+ if (groups.zulu === undefined) {
127
+ const offHour = Number(groups.offHour);
128
+ const offMinute = Number(groups.offMinute);
129
+ if (offHour > 23 || offMinute > 59)
130
+ return false;
131
+ if (groups.offSign === '-' && offHour === 0 && offMinute === 0)
132
+ return false;
133
+ }
134
+ return true;
135
+ }
136
+ function resolveTaskStateAge(updatedAt, now) {
137
+ if (!updatedAt)
138
+ return { known: false };
139
+ const match = ZONED_INSTANT.exec(updatedAt);
140
+ if (!match?.groups || !isRealInstant(match.groups))
141
+ return { known: false };
142
+ const then = Date.parse(updatedAt);
143
+ if (Number.isNaN(then))
144
+ return { known: false };
145
+ const hours = (now.getTime() - then) / 3_600_000;
146
+ if (hours < -(CLOCK_SKEW_ALLOWANCE_MINUTES / 60))
147
+ return { known: false };
148
+ return { known: true, hours: Math.max(0, hours) };
149
+ }
150
+ function staleTaskStateLine(project, hours) {
151
+ const days = Math.floor(hours / 24);
152
+ const age = days >= 1 ? `${days} day${days === 1 ? '' : 's'} ago` : `${Math.floor(hours)} hour${Math.floor(hours) === 1 ? '' : 's'} ago`;
153
+ return `Task state for "${project}" was last stated ${age} — older than ${STALE_TASK_STATE_HOURS}h, so it is not shown as current. Run \`memesh task\` to see or update it.`;
154
+ }
155
+ function taskStateAgeUnknownLine(project) {
156
+ return `Task state for "${project}" has a missing, unreadable, or future-dated timestamp, so its age could not be established — not shown as current. Run \`memesh task\` to see or update it.`;
157
+ }
158
+ export function briefingTaskStateLines(state, project, now = new Date(), { includeFresh = true } = {}) {
159
+ if (isEmptyTaskState(state))
160
+ return [];
161
+ const age = resolveTaskStateAge(state.updated_at, now);
162
+ if (!age.known)
163
+ return [taskStateAgeUnknownLine(project)];
164
+ if (age.hours > STALE_TASK_STATE_HOURS) {
165
+ return [staleTaskStateLine(project, age.hours)];
166
+ }
167
+ return includeFresh ? taskStateLines(state, project, now) : [];
168
+ }