memorix 1.2.0 → 1.2.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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,85 +1,85 @@
1
- /**
2
- * Git hooks path resolver — worktree-safe.
3
- *
4
- * In a normal repo: .git is a directory → hooks at .git/hooks/
5
- * In a git worktree: .git is a FILE containing "gitdir: /path/to/actual/git/dir"
6
- * → hooks at that resolved path + /hooks/
7
- *
8
- * This utility ensures all hook install/uninstall/check operations
9
- * work correctly in both scenarios.
10
- */
11
-
12
- import { existsSync, readFileSync, statSync, mkdirSync } from 'node:fs';
13
- import path from 'node:path';
14
-
15
- /**
16
- * Resolve the actual .git directory path, following worktree indirection.
17
- *
18
- * @param projectRoot - The root of the working tree (where .git lives)
19
- * @returns Absolute path to the real git dir, or null if no .git found
20
- */
21
- export function resolveGitDir(projectRoot: string): string | null {
22
- const dotGit = path.join(projectRoot, '.git');
23
-
24
- if (!existsSync(dotGit)) return null;
25
-
26
- const stat = statSync(dotGit);
27
-
28
- if (stat.isDirectory()) {
29
- // Normal repo — .git is a directory
30
- return dotGit;
31
- }
32
-
33
- if (stat.isFile()) {
34
- // Worktree — .git is a file: "gitdir: /path/to/actual/git/dir"
35
- try {
36
- const content = readFileSync(dotGit, 'utf-8').trim();
37
- const match = content.match(/^gitdir:\s*(.+)$/);
38
- if (match) {
39
- const gitdir = match[1].trim();
40
- // Resolve relative paths against the project root
41
- const resolved = path.isAbsolute(gitdir)
42
- ? gitdir
43
- : path.resolve(projectRoot, gitdir);
44
- if (existsSync(resolved)) {
45
- return resolved;
46
- }
47
- }
48
- } catch {
49
- // Unreadable .git file — fall through
50
- }
51
- }
52
-
53
- return null;
54
- }
55
-
56
- /**
57
- * Resolve the hooks directory for a project (read-only — does NOT create directories).
58
- * Handles both normal repos and git worktrees.
59
- *
60
- * @param projectRoot - The root of the working tree
61
- * @returns Object with hooksDir and hookPath for post-commit, or null if no .git found
62
- */
63
- export function resolveHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
64
- const gitDir = resolveGitDir(projectRoot);
65
- if (!gitDir) return null;
66
-
67
- const hooksDir = path.join(gitDir, 'hooks');
68
-
69
- return {
70
- hooksDir,
71
- hookPath: path.join(hooksDir, 'post-commit'),
72
- };
73
- }
74
-
75
- /**
76
- * Resolve hooks directory AND ensure it exists on disk.
77
- * Use this only in write paths (hook install / autoHook).
78
- */
79
- export function ensureHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
80
- const resolved = resolveHooksDir(projectRoot);
81
- if (!resolved) return null;
82
-
83
- mkdirSync(resolved.hooksDir, { recursive: true });
84
- return resolved;
85
- }
1
+ /**
2
+ * Git hooks path resolver — worktree-safe.
3
+ *
4
+ * In a normal repo: .git is a directory → hooks at .git/hooks/
5
+ * In a git worktree: .git is a FILE containing "gitdir: /path/to/actual/git/dir"
6
+ * → hooks at that resolved path + /hooks/
7
+ *
8
+ * This utility ensures all hook install/uninstall/check operations
9
+ * work correctly in both scenarios.
10
+ */
11
+
12
+ import { existsSync, readFileSync, statSync, mkdirSync } from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ /**
16
+ * Resolve the actual .git directory path, following worktree indirection.
17
+ *
18
+ * @param projectRoot - The root of the working tree (where .git lives)
19
+ * @returns Absolute path to the real git dir, or null if no .git found
20
+ */
21
+ export function resolveGitDir(projectRoot: string): string | null {
22
+ const dotGit = path.join(projectRoot, '.git');
23
+
24
+ if (!existsSync(dotGit)) return null;
25
+
26
+ const stat = statSync(dotGit);
27
+
28
+ if (stat.isDirectory()) {
29
+ // Normal repo — .git is a directory
30
+ return dotGit;
31
+ }
32
+
33
+ if (stat.isFile()) {
34
+ // Worktree — .git is a file: "gitdir: /path/to/actual/git/dir"
35
+ try {
36
+ const content = readFileSync(dotGit, 'utf-8').trim();
37
+ const match = content.match(/^gitdir:\s*(.+)$/);
38
+ if (match) {
39
+ const gitdir = match[1].trim();
40
+ // Resolve relative paths against the project root
41
+ const resolved = path.isAbsolute(gitdir)
42
+ ? gitdir
43
+ : path.resolve(projectRoot, gitdir);
44
+ if (existsSync(resolved)) {
45
+ return resolved;
46
+ }
47
+ }
48
+ } catch {
49
+ // Unreadable .git file — fall through
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Resolve the hooks directory for a project (read-only — does NOT create directories).
58
+ * Handles both normal repos and git worktrees.
59
+ *
60
+ * @param projectRoot - The root of the working tree
61
+ * @returns Object with hooksDir and hookPath for post-commit, or null if no .git found
62
+ */
63
+ export function resolveHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
64
+ const gitDir = resolveGitDir(projectRoot);
65
+ if (!gitDir) return null;
66
+
67
+ const hooksDir = path.join(gitDir, 'hooks');
68
+
69
+ return {
70
+ hooksDir,
71
+ hookPath: path.join(hooksDir, 'post-commit'),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Resolve hooks directory AND ensure it exists on disk.
77
+ * Use this only in write paths (hook install / autoHook).
78
+ */
79
+ export function ensureHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
80
+ const resolved = resolveHooksDir(projectRoot);
81
+ if (!resolved) return null;
82
+
83
+ mkdirSync(resolved.hooksDir, { recursive: true });
84
+ return resolved;
85
+ }
@@ -0,0 +1,117 @@
1
+ import type { ObservationAdmissionState, ObservationType } from '../types.js';
2
+ import { isSignificantKnowledge } from './significance-filter.js';
3
+ import type { NormalizedHookInput } from './types.js';
4
+
5
+ export type HookCaptureCategory =
6
+ | 'file_modify'
7
+ | 'file_read'
8
+ | 'command'
9
+ | 'search'
10
+ | 'memorix_internal'
11
+ | 'unknown';
12
+
13
+ export type HookAdmissionDecision =
14
+ | { action: 'drop'; reason: string }
15
+ | {
16
+ action: 'store';
17
+ admissionState: ObservationAdmissionState;
18
+ admissionReason: string;
19
+ valueCategory: 'core' | 'contextual' | 'ephemeral';
20
+ };
21
+
22
+ const FAILURE_PATTERN = /\b(fail(?:ed|ure)?|error|exception|assert(?:ion)?|exit code [1-9]|TypeError|ReferenceError|SyntaxError|ENOENT|ECONNREFUSED|panic)\b/i;
23
+ const DECISION_PATTERN = /\b(decision|decided|choose|chosen|because|trade-?off|invariant|root cause|workaround|must|should not)\b/i;
24
+ const TASK_PATTERN = /\b(add|implement|fix|refactor|migrate|upgrade|debug|investigate|test|release|deploy|remove)\b/i;
25
+ const VALIDATION_PATTERN = /\b(test|tests|vitest|jest|pytest|playwright|cypress|typecheck|lint|build|cargo test|go test)\b/i;
26
+ const FOCUSED_VALIDATION_PATTERN = /(?:--run|--filter|--grep|\s-t\s|\.test\.|\.spec\.|::[\w-]+)/i;
27
+ const ROUTINE_SUCCESS_PATTERN = /\b(added \d+ packages|up to date|passed|all tests pass|tests? passed|0 failures|success(?:fully)?)\b/i;
28
+
29
+ function isCandidateType(type: ObservationType): boolean {
30
+ return type === 'decision' ||
31
+ type === 'gotcha' ||
32
+ type === 'problem-solution' ||
33
+ type === 'trade-off' ||
34
+ type === 'why-it-exists';
35
+ }
36
+
37
+ function candidate(
38
+ admissionReason: string,
39
+ valueCategory: 'core' | 'contextual' = 'contextual',
40
+ ): HookAdmissionDecision {
41
+ return { action: 'store', admissionState: 'candidate', admissionReason, valueCategory };
42
+ }
43
+
44
+ function ephemeral(admissionReason: string): HookAdmissionDecision {
45
+ return { action: 'store', admissionState: 'ephemeral', admissionReason, valueCategory: 'ephemeral' };
46
+ }
47
+
48
+ /**
49
+ * Cheap, deterministic first-pass admission for cross-agent hooks. It never
50
+ * promotes an automatic record directly to durable context: background
51
+ * qualification must still prove a current Code Memory reference.
52
+ */
53
+ export function assessHookAdmission(input: {
54
+ hook: NormalizedHookInput;
55
+ category: HookCaptureCategory;
56
+ content: string;
57
+ observationType: ObservationType;
58
+ }): HookAdmissionDecision {
59
+ const { hook, category, content, observationType } = input;
60
+ const command = hook.command ?? '';
61
+ const hasFailure = FAILURE_PATTERN.test(content);
62
+ const hasDecision = DECISION_PATTERN.test(content) || isCandidateType(observationType);
63
+ const isValidation = VALIDATION_PATTERN.test(command) || VALIDATION_PATTERN.test(content);
64
+ const isFocusedValidation = FOCUSED_VALIDATION_PATTERN.test(command);
65
+ const isRoutineSuccess = ROUTINE_SUCCESS_PATTERN.test(content);
66
+
67
+ if (hasFailure) {
68
+ return candidate('automatic capture contains a concrete failure or verification signal');
69
+ }
70
+ if (hasDecision) {
71
+ return candidate('automatic capture contains a decision or durable rationale', 'core');
72
+ }
73
+
74
+ if (hook.event === 'user_prompt') {
75
+ return TASK_PATTERN.test(content)
76
+ ? candidate('user task contains a concrete technical action')
77
+ : { action: 'drop', reason: 'prompt has no durable technical task signal' };
78
+ }
79
+
80
+ if (hook.event === 'post_response') {
81
+ // Agent integrations such as OpenCode emit concise end-of-turn summaries.
82
+ // Keep technically meaningful ones as candidates; qualification still
83
+ // prevents them from becoming automatic context without code evidence.
84
+ return isSignificantKnowledge(content).isSignificant
85
+ ? candidate('assistant response records a concrete technical outcome')
86
+ : { action: 'drop', reason: 'response has no durable technical outcome' };
87
+ }
88
+
89
+ if (hook.event === 'session_end') {
90
+ // The handler already applies a minimum-content gate for session end.
91
+ // Preserve a compact handoff trace, but keep it out of automatic context
92
+ // until later qualification.
93
+ return candidate('session-end summary awaits Code Memory qualification');
94
+ }
95
+
96
+ if (category === 'file_modify') {
97
+ return candidate('file mutation awaits Code Memory qualification');
98
+ }
99
+
100
+ if (category === 'command') {
101
+ if (isValidation && isFocusedValidation && !isRoutineSuccess) {
102
+ return candidate('focused validation result awaits Code Memory qualification');
103
+ }
104
+ if (isValidation || isRoutineSuccess) {
105
+ return ephemeral('routine command result retained only as a short-lived trace');
106
+ }
107
+ return ephemeral('command activity retained only as a short-lived trace');
108
+ }
109
+
110
+ if (category === 'search' || category === 'unknown') {
111
+ return content.length >= 300
112
+ ? ephemeral('unverified automatic activity retained only as a short-lived trace')
113
+ : { action: 'drop', reason: 'automatic activity has insufficient evidence' };
114
+ }
115
+
116
+ return { action: 'drop', reason: 'capture category is not eligible for automatic storage' };
117
+ }
@@ -2,15 +2,21 @@
2
2
  * Hook Handler
3
3
  *
4
4
  * Unified entry point for all agent hooks.
5
- * Architecture: Normalize → Classify → Policy → Store → Respond
5
+ * Architecture: Normalize → Classify → Admit → Store → Respond
6
6
  *
7
7
  * Design principles (inspired by claude-mem + mcp-memory-service):
8
- * - Store-first: capture generously, filter at read time
8
+ * - Candidate-first: automatic capture is never durable context by default
9
9
  * - Tool Taxonomy: declarative policies per tool category
10
10
  * - Pattern = classification only: determines observation type, not storage
11
11
  */
12
12
 
13
+ import { createHash } from 'node:crypto';
13
14
  import type { ObservationType } from '../types.js';
15
+ import {
16
+ assessHookAdmission,
17
+ type HookAdmissionDecision,
18
+ type HookCaptureCategory,
19
+ } from './admission.js';
14
20
  import { normalizeHookInput } from './normalizer.js';
15
21
  import { detectBestPattern, patternToObservationType } from './pattern-detector.js';
16
22
  import { isSignificantKnowledge, isRetrievedResult, isTrivialCommand } from './significance-filter.js';
@@ -35,6 +41,11 @@ const MIN_PROMPT_LENGTH = 20;
35
41
  /** Max content length (truncate beyond this) */
36
42
  const MAX_CONTENT_LENGTH = 4000;
37
43
 
44
+ function deriveHookActorId(input: NormalizedHookInput): string {
45
+ const material = `${input.agent ?? 'unknown'}\u0000${input.sessionId ?? 'unknown'}`;
46
+ return `hook:${createHash('sha256').update(material).digest('hex').slice(0, 24)}`;
47
+ }
48
+
38
49
  /** Truly trivial commands — standalone navigation/inspection only */
39
50
  const NOISE_COMMANDS = [
40
51
  /^(ls|dir|cd|pwd|echo|cat|type|head|tail|wc|which|where|whoami)(\s|$)/i,
@@ -49,7 +60,7 @@ const NOISE_COMMANDS = [
49
60
  // ─── Tool Taxonomy ───
50
61
 
51
62
  /** Tool categories for storage policy */
52
- type ToolCategory = 'file_modify' | 'file_read' | 'command' | 'search' | 'memorix_internal' | 'unknown';
63
+ type ToolCategory = HookCaptureCategory;
53
64
 
54
65
  /** Storage policy per tool category */
55
66
  interface StoragePolicy {
@@ -233,7 +244,12 @@ function generateTitle(input: NormalizedHookInput, patternType: string): string
233
244
  return `Activity (${patternType})`;
234
245
  }
235
246
 
236
- function buildObservation(input: NormalizedHookInput, content: string, category: ToolCategory) {
247
+ function buildObservation(
248
+ input: NormalizedHookInput,
249
+ content: string,
250
+ category: ToolCategory,
251
+ admission?: Extract<HookAdmissionDecision, { action: 'store' }>,
252
+ ) {
237
253
  const pattern = detectBestPattern(content);
238
254
  const policy = STORAGE_POLICY[category] ?? STORAGE_POLICY.unknown;
239
255
  const fallbackType = input.filePath ? 'what-changed' : policy.defaultType;
@@ -252,6 +268,15 @@ function buildObservation(input: NormalizedHookInput, content: string, category:
252
268
  ],
253
269
  concepts: pattern?.matchedKeywords ?? [],
254
270
  filesModified: input.filePath ? [input.filePath] : [],
271
+ ...(admission ? {
272
+ valueCategory: admission.valueCategory,
273
+ admissionState: admission.admissionState,
274
+ admissionReason: admission.admissionReason,
275
+ // Automatic capture is private until current code can qualify it for
276
+ // shared project delivery. The opaque actor is stable within a hook session.
277
+ visibility: 'personal' as const,
278
+ createdByAgentId: deriveHookActorId(input),
279
+ } : {}),
255
280
  };
256
281
  }
257
282
 
@@ -281,7 +306,9 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
281
306
  const { initMiniSkillStore } = await import('../store/mini-skill-store.js');
282
307
  const { initSessionStore } = await import('../store/session-store.js');
283
308
  const { initAliasRegistry, registerAlias } = await import('../project/aliases.js');
309
+ const { MaintenanceTargetStore } = await import('../runtime/maintenance-targets.js');
284
310
  const { buildAutoProjectContext, formatAutoProjectContextPrompt } = await import('../codegraph/auto-context.js');
311
+ const { filterReadableObservations } = await import('../memory/visibility.js');
285
312
 
286
313
  const rawProject = detectProject(input.cwd || process.cwd());
287
314
  if (!rawProject) throw new Error('No .git found');
@@ -289,10 +316,18 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
289
316
 
290
317
  initAliasRegistry(dataDir);
291
318
  const canonicalId = await registerAlias(rawProject);
319
+ new MaintenanceTargetStore(dataDir).register({
320
+ projectId: canonicalId,
321
+ projectRoot: rawProject.rootPath,
322
+ dataDir,
323
+ });
292
324
  await initObservationStore(dataDir);
293
325
  await initMiniSkillStore(dataDir);
294
326
  await initSessionStore(dataDir);
295
- const activeObservations = await getStore().loadByProject(canonicalId, { status: 'active' });
327
+ const activeObservations = filterReadableObservations(
328
+ await getStore().loadByProject(canonicalId, { status: 'active' }),
329
+ { projectId: canonicalId },
330
+ );
296
331
  const context = await buildAutoProjectContext({
297
332
  project: { ...rawProject, id: canonicalId },
298
333
  dataDir,
@@ -306,6 +341,7 @@ async function handleSessionStart(input: NormalizedHookInput): Promise<{
306
341
  maxFiles: 5_000,
307
342
  });
308
343
  }),
344
+ deliveryTarget: 'hook-session-start',
309
345
  });
310
346
  contextSummary = `\n\n${formatAutoProjectContextPrompt(context)}`;
311
347
  } catch (sessErr) {
@@ -352,8 +388,17 @@ async function handleHookEventCore(input: NormalizedHookInput): Promise<{
352
388
  if (endContent.length < 50) {
353
389
  return { observation: null, output: defaultOutput };
354
390
  }
391
+ const draft = buildObservation(input, endContent, 'unknown');
392
+ const admission = assessHookAdmission({
393
+ hook: input,
394
+ category: 'unknown',
395
+ content: endContent,
396
+ observationType: draft.type,
397
+ });
355
398
  return {
356
- observation: buildObservation(input, endContent, 'unknown'),
399
+ observation: admission.action === 'store'
400
+ ? buildObservation(input, endContent, 'unknown', admission)
401
+ : null,
357
402
  output: defaultOutput,
358
403
  };
359
404
  }
@@ -425,8 +470,17 @@ async function handleHookEventCore(input: NormalizedHookInput): Promise<{
425
470
  }
426
471
  markTriggered(cooldownKey);
427
472
 
473
+ const draft = buildObservation(input, content, category);
474
+ const admission = assessHookAdmission({
475
+ hook: input,
476
+ category,
477
+ content,
478
+ observationType: draft.type,
479
+ });
428
480
  return {
429
- observation: buildObservation(input, content, category),
481
+ observation: admission.action === 'store'
482
+ ? buildObservation(input, content, category, admission)
483
+ : null,
430
484
  output: defaultOutput,
431
485
  };
432
486
  }
@@ -440,17 +494,24 @@ async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Pro
440
494
  { getProjectDataDir },
441
495
  { initAliasRegistry, registerAlias },
442
496
  { enqueueCodegraphRefresh },
497
+ { MaintenanceTargetStore },
443
498
  ] = await Promise.all([
444
499
  import('../project/detector.js'),
445
500
  import('../store/persistence.js'),
446
501
  import('../project/aliases.js'),
447
502
  import('../runtime/lifecycle.js'),
503
+ import('../runtime/maintenance-targets.js'),
448
504
  ]);
449
505
  const project = detectProject(input.cwd || process.cwd());
450
506
  if (!project) return;
451
507
  const dataDir = await getProjectDataDir(project.id);
452
508
  initAliasRegistry(dataDir);
453
509
  const projectId = await registerAlias(project);
510
+ new MaintenanceTargetStore(dataDir).register({
511
+ projectId,
512
+ projectRoot: project.rootPath,
513
+ dataDir,
514
+ });
454
515
  enqueueCodegraphRefresh({
455
516
  dataDir,
456
517
  projectId,
@@ -465,11 +526,25 @@ async function queueCodegraphRefreshForMutation(input: NormalizedHookInput): Pro
465
526
  export async function handleHookEvent(input: NormalizedHookInput): Promise<{
466
527
  observation: ReturnType<typeof buildObservation> | null;
467
528
  output: HookOutput;
529
+ }>;
530
+ export async function handleHookEvent(input: NormalizedHookInput, options: {
531
+ deferMaintenance?: boolean;
532
+ }): Promise<{
533
+ observation: ReturnType<typeof buildObservation> | null;
534
+ output: HookOutput;
535
+ }>;
536
+ export async function handleHookEvent(input: NormalizedHookInput, options: {
537
+ deferMaintenance?: boolean;
538
+ } = {}): Promise<{
539
+ observation: ReturnType<typeof buildObservation> | null;
540
+ output: HookOutput;
468
541
  }> {
469
542
  try {
470
543
  return await handleHookEventCore(input);
471
544
  } finally {
472
- await queueCodegraphRefreshForMutation(input);
545
+ // The CLI persists an automatic observation after this function returns.
546
+ // It defers scheduling so a fast worker never scans before that write.
547
+ if (!options.deferMaintenance) await queueCodegraphRefreshForMutation(input);
473
548
  }
474
549
  }
475
550
 
@@ -570,7 +645,7 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
570
645
  }
571
646
 
572
647
  const input = normalizeHookInput(payload);
573
- const { observation, output } = await handleHookEvent(input);
648
+ const { observation, output } = await handleHookEvent(input, { deferMaintenance: true });
574
649
 
575
650
  if (observation) {
576
651
  try {
@@ -581,6 +656,7 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
581
656
  const { detectProject } = await import('../project/detector.js');
582
657
  const { getProjectDataDir } = await import('../store/persistence.js');
583
658
  const { initAliasRegistry, registerAlias } = await import('../project/aliases.js');
659
+ const { MaintenanceTargetStore } = await import('../runtime/maintenance-targets.js');
584
660
 
585
661
  const rawProject = detectProject(input.cwd || process.cwd());
586
662
  if (!rawProject) throw new Error('No .git found');
@@ -590,94 +666,20 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
590
666
  initAliasRegistry(dataDir);
591
667
  const canonicalId = await registerAlias(rawProject);
592
668
  const projectId = canonicalId;
669
+ new MaintenanceTargetStore(dataDir).register({
670
+ projectId,
671
+ projectRoot: rawProject.rootPath,
672
+ dataDir,
673
+ });
593
674
 
594
675
  await initObservationStore(dataDir);
595
676
  await initMSStore(dataDir);
596
677
  await initSessStore(dataDir);
597
678
  await initObservations(dataDir);
598
679
  await storeObservation({ ...observation, projectId, sourceDetail: 'hook' });
599
-
600
- // Shadow mode: Formation Pipeline metrics (fire-and-forget, never blocks)
601
- try {
602
- const { runFormation } = await import('../memory/formation/index.js');
603
- const formationMode = (process.env.MEMORIX_FORMATION_MODE as 'shadow' | 'active' | 'fallback') || 'shadow';
604
- const samplingRate = parseFloat(process.env.MEMORIX_FORMATION_HOOKS_SAMPLING_RATE || '0.1');
605
- const shouldSample = Math.random() < samplingRate;
606
-
607
- if (shouldSample) {
608
- const { withFreshIndex } = await import('../memory/freshness.js');
609
- const { getAllObservations } = await import('../memory/observations.js');
610
- await withFreshIndex(() => getAllObservations());
611
- }
612
-
613
- // In hooks, shadow mode by default for performance
614
- // Sampling rate controls how often we run full resolve (expensive)
615
- const searchFn = shouldSample
616
- ? async (q: string, limit: number, pid: string) => {
617
- const { compactSearch, compactDetail } = await import('../compact/engine.js');
618
- const result = await compactSearch({ query: q, limit, projectId: pid, status: 'active' });
619
- if (result.entries.length === 0) return [];
620
- const details = await compactDetail(result.entries.map(e => e.id));
621
- return details.documents.map((d, i) => ({
622
- id: Number(d.id.replace('obs-', '')),
623
- observationId: d.observationId,
624
- title: d.title,
625
- narrative: d.narrative,
626
- facts: d.facts,
627
- entityName: d.entityName,
628
- type: d.type,
629
- score: result.entries[i]?.score ?? 0,
630
- }));
631
- }
632
- : async () => []; // Skip search for speed (shadow mode)
633
-
634
- const getObsFn = shouldSample
635
- ? (id: number) => {
636
- const { getObservation } = require('../memory/observations.js');
637
- const o = getObservation(id);
638
- if (!o) return null;
639
- return {
640
- id: o.id,
641
- entityName: o.entityName,
642
- type: o.type,
643
- title: o.title,
644
- narrative: o.narrative,
645
- facts: o.facts,
646
- topicKey: o.topicKey,
647
- };
648
- }
649
- : () => null;
650
-
651
- const getEntityNamesFn = shouldSample
652
- ? () => {
653
- const { graphManager } = require('../memory/graph.js');
654
- return graphManager.getEntityNames();
655
- }
656
- : () => [];
657
-
658
- runFormation({
659
- entityName: observation.entityName,
660
- type: observation.type,
661
- title: observation.title,
662
- narrative: observation.narrative,
663
- facts: observation.facts,
664
- projectId,
665
- source: 'hook' as const,
666
- }, {
667
- mode: formationMode,
668
- useLLM: false,
669
- minValueScore: 0.3,
670
- hooksSamplingRate: samplingRate,
671
- searchMemories: searchFn,
672
- getObservation: getObsFn,
673
- getEntityNames: getEntityNamesFn,
674
- }).catch(() => {});
675
- } catch { /* Formation is optional — never break hooks */ }
676
-
677
- // Feedback: tell the agent what was saved
678
- const emoji = TYPE_EMOJI[observation.type] ?? '[PLAN]';
679
- output.systemMessage = (output.systemMessage ?? '') +
680
- `\n${emoji} Memorix saved: ${observation.title} [${observation.type}]`;
680
+ // Automatic capture is deliberately quiet. Candidate state and later
681
+ // qualification are visible through Memorix inspection, not injected as
682
+ // a stream of status messages into the host agent's context.
681
683
  } catch (storeErr) {
682
684
  // Diagnostic log — hooks must never break the agent, but silent
683
685
  // swallow makes end-to-end debugging impossible.
@@ -685,6 +687,11 @@ export async function runHook(agentOverride?: string, eventOverride?: string): P
685
687
  }
686
688
  }
687
689
 
690
+ // A candidate must be durable before a Code Memory refresh can qualify it.
691
+ // Keep direct handleHookEvent() backward-compatible, but make the real CLI
692
+ // hook path explicitly capture first and schedule second.
693
+ await queueCodegraphRefreshForMutation(input);
694
+
688
695
  // Build hookSpecificOutput — Claude Code only supports it for 3 event types:
689
696
  // PreToolUse, UserPromptSubmit, PostToolUse
690
697
  // Other events (SessionStart, Stop, PreCompact) must NOT include hookSpecificOutput.