freeharness 0.4.1 → 0.4.3

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 (203) hide show
  1. package/NOTICE +2 -1
  2. package/README.md +24 -19
  3. package/canonical/HARVEST.yaml +27 -16
  4. package/canonical/PROVENANCE.yaml +3 -3
  5. package/canonical/agents/README.md +77 -36
  6. package/canonical/agents/agentic-architect.md +2 -1
  7. package/canonical/agents/database-reviewer.md +6 -8
  8. package/canonical/agents/explorer-ai-engineer-analyst.md +2 -6
  9. package/canonical/agents/implementer.md +30 -0
  10. package/canonical/agents/pr-reviewer.md +9 -11
  11. package/canonical/agents/python-reviewer.md +7 -9
  12. package/canonical/agents/report-formatter.md +7 -3
  13. package/canonical/agents/security-reviewer.md +14 -9
  14. package/canonical/agents/test-writer.md +5 -2
  15. package/canonical/hooks/cost-prices.json +11 -9
  16. package/canonical/hooks/freeharness-config-guard.mjs +4155 -93
  17. package/canonical/hooks/freeharness-context-monitor.mjs +88 -54
  18. package/canonical/hooks/freeharness-converge.mjs +78 -23
  19. package/canonical/hooks/freeharness-cost-tracker.mjs +1116 -218
  20. package/canonical/hooks/freeharness-memory-guard.mjs +20 -23
  21. package/canonical/hooks/freeharness-precompact.mjs +206 -87
  22. package/canonical/hooks/freeharness-quota-tap.mjs +173 -0
  23. package/canonical/hooks/freeharness-reload-context.mjs +197 -0
  24. package/canonical/hooks/freeharness-repeat-tool.mjs +1 -1
  25. package/canonical/hooks/freeharness-sdd-context.mjs +959 -0
  26. package/canonical/hooks/freeharness-session-start +297 -59
  27. package/canonical/hooks/hooks.claude-code.json +1 -1
  28. package/canonical/hooks/hooks.config-guard.claude-code.json +2 -2
  29. package/canonical/hooks/hooks.context-monitor.claude-code.json +1 -1
  30. package/canonical/hooks/hooks.converge.claude-code.json +1 -1
  31. package/canonical/hooks/hooks.memory-guard.claude-code.json +1 -1
  32. package/canonical/hooks/hooks.reload-context.claude-code.json +15 -0
  33. package/canonical/openspec/schema.spec-driven.yaml +29 -8
  34. package/canonical/openspec/templates/follow-ups.md +3 -2
  35. package/canonical/openspec/templates/proposal.md +1 -1
  36. package/canonical/openspec/templates/tasks.md +8 -0
  37. package/canonical/rules/CLAUDE.md +5 -4
  38. package/canonical/rules/routing.md +10 -9
  39. package/canonical/skills/agent-evaluation/SKILL.md +17 -6
  40. package/canonical/skills/agent-evaluation/evals/cases.yaml +38 -23
  41. package/canonical/skills/agentic-architecture/SKILL.md +4 -0
  42. package/canonical/skills/agentic-architecture/evals/cases.yaml +27 -0
  43. package/canonical/skills/code-review/SKILL.md +10 -3
  44. package/canonical/skills/code-review/evals/cases.yaml +1118 -27
  45. package/canonical/skills/git-workflow/SKILL.md +2 -0
  46. package/canonical/skills/local-code/SKILL.md +2 -2
  47. package/canonical/skills/local-code/evals/cases.yaml +1 -1
  48. package/canonical/skills/report-formatting/SKILL.md +10 -2
  49. package/canonical/skills/report-formatting/reference/ARCHITECTURE.md +3 -3
  50. package/canonical/skills/report-formatting/reference/HELPERS.md +10 -5
  51. package/canonical/skills/report-formatting/reference/RELEASE.md +1 -2
  52. package/canonical/skills/report-formatting/reference/TEMPLATES.md +3 -2
  53. package/canonical/skills/report-formatting/reference/authoring.md +25 -37
  54. package/canonical/skills/report-formatting/scripts/build-architecture.js +1 -1
  55. package/canonical/skills/report-formatting/scripts/build-md-claro.js +5 -8
  56. package/canonical/skills/report-formatting/scripts/check-env.js +0 -4
  57. package/canonical/skills/report-formatting/scripts/docmodel.js +3 -11
  58. package/canonical/skills/report-formatting/scripts/example-build.js +8 -10
  59. package/canonical/skills/report-formatting/scripts/example_build_ArchitectureReview_docx.js +8 -10
  60. package/canonical/skills/report-formatting/scripts/example_build_ExecutiveReport_docx.js +8 -10
  61. package/canonical/skills/report-formatting/scripts/theme-claro.js +39 -10
  62. package/canonical/skills/subagent-driven-development/SKILL.md +35 -28
  63. package/canonical/skills/subagent-driven-development/evals/cases.yaml +3625 -19
  64. package/canonical/skills/subagent-driven-development/reference/brief.md +187 -35
  65. package/canonical/skills/subagent-driven-development/reference/handoff.md +96 -2
  66. package/canonical/skills/subagent-driven-development/reference/loop.md +56 -0
  67. package/canonical/skills/subagent-driven-development/reference/trees.md +40 -0
  68. package/canonical/skills/subagent-driven-development/reference/workspace.md +31 -0
  69. package/canonical/skills/subagent-driven-development/scripts/validate-handoff.mjs +4035 -327
  70. package/dist/adapters/harness/claude-code.d.ts +26 -3
  71. package/dist/adapters/harness/claude-code.js +214 -43
  72. package/dist/adapters/harness/opencode.js +17 -3
  73. package/dist/adapters/harness/plugin.js +28 -17
  74. package/dist/adapters/memory/doctor.d.ts +172 -33
  75. package/dist/adapters/memory/doctor.js +750 -139
  76. package/dist/adapters/memory/migrate.d.ts +95 -0
  77. package/dist/adapters/memory/migrate.js +497 -0
  78. package/dist/adapters/memory/paths.d.ts +15 -0
  79. package/dist/adapters/memory/paths.js +47 -0
  80. package/dist/adapters/memory/vendored.d.ts +52 -30
  81. package/dist/adapters/memory/vendored.js +161 -169
  82. package/dist/adapters/memory/worker-control.d.ts +9 -0
  83. package/dist/adapters/memory/worker-control.js +30 -0
  84. package/dist/adapters/sync/diff.js +4 -1
  85. package/dist/cli/cleanup-prompt.d.ts +35 -0
  86. package/dist/cli/cleanup-prompt.js +68 -0
  87. package/dist/cli/cleanup.d.ts +22 -24
  88. package/dist/cli/cleanup.js +204 -75
  89. package/dist/cli/cost-quota.d.ts +158 -0
  90. package/dist/cli/cost-quota.js +645 -0
  91. package/dist/cli/cost.d.ts +408 -46
  92. package/dist/cli/cost.js +1731 -57
  93. package/dist/cli/doctor.d.ts +52 -13
  94. package/dist/cli/doctor.js +199 -45
  95. package/dist/cli/hard-cleanup.d.ts +93 -0
  96. package/dist/cli/hard-cleanup.js +552 -0
  97. package/dist/cli/install-facts.d.ts +29 -0
  98. package/dist/cli/install-facts.js +68 -0
  99. package/dist/cli/json-key-edit.d.ts +18 -0
  100. package/dist/cli/json-key-edit.js +118 -0
  101. package/dist/cli/memory-import.d.ts +74 -0
  102. package/dist/cli/memory-import.js +469 -0
  103. package/dist/cli/plugin-validate.d.ts +12 -0
  104. package/dist/cli/plugin-validate.js +43 -0
  105. package/dist/cli/profile.d.ts +1 -1
  106. package/dist/cli/profile.js +10 -10
  107. package/dist/cli/uninstall.d.ts +1 -1
  108. package/dist/cli/uninstall.js +2 -2
  109. package/dist/cli/upstream-launchers.d.ts +49 -0
  110. package/dist/cli/upstream-launchers.js +287 -0
  111. package/dist/index.js +252 -73
  112. package/dist/infra/fsutil.d.ts +30 -3
  113. package/dist/infra/fsutil.js +68 -6
  114. package/dist/infra/manifest.d.ts +2 -1
  115. package/dist/infra/manifest.js +3 -2
  116. package/dist/infra/prices.d.ts +18 -0
  117. package/dist/infra/prices.js +64 -0
  118. package/node_modules/@freeharness/core/dist/registry.d.ts +1 -1
  119. package/node_modules/@freeharness/core/dist/registry.js +3 -3
  120. package/node_modules/@freeharness/core/dist/tiers.d.ts +10 -1
  121. package/node_modules/@freeharness/core/dist/tiers.js +24 -2
  122. package/node_modules/@freeharness/core/package.json +1 -1
  123. package/node_modules/@freeharness/sdd/PROVENANCE.yaml +279 -2
  124. package/node_modules/@freeharness/sdd/dist/src/bin/openspec.js +34 -8
  125. package/node_modules/@freeharness/sdd/dist/src/shim/archive.d.ts +7 -6
  126. package/node_modules/@freeharness/sdd/dist/src/shim/archive.js +72 -64
  127. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.d.ts +48 -16
  128. package/node_modules/@freeharness/sdd/dist/src/shim/follow-up.js +168 -70
  129. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.d.ts +36 -6
  130. package/node_modules/@freeharness/sdd/dist/src/shim/instructions.js +110 -49
  131. package/node_modules/@freeharness/sdd/dist/src/shim/list.d.ts +1 -1
  132. package/node_modules/@freeharness/sdd/dist/src/shim/list.js +57 -14
  133. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.d.ts +7 -0
  134. package/node_modules/@freeharness/sdd/dist/src/shim/nested-change.js +118 -0
  135. package/node_modules/@freeharness/sdd/dist/src/shim/new-change.js +3 -1
  136. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.d.ts +12 -0
  137. package/node_modules/@freeharness/sdd/dist/src/shim/proposal-capabilities.js +94 -0
  138. package/node_modules/@freeharness/sdd/dist/src/shim/shared.d.ts +17 -0
  139. package/node_modules/@freeharness/sdd/dist/src/shim/shared.js +182 -1
  140. package/node_modules/@freeharness/sdd/dist/src/shim/status.d.ts +2 -2
  141. package/node_modules/@freeharness/sdd/dist/src/shim/status.js +34 -5
  142. package/node_modules/@freeharness/sdd/dist/src/shim/validate.d.ts +9 -0
  143. package/node_modules/@freeharness/sdd/dist/src/shim/validate.js +49 -41
  144. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.d.ts +21 -0
  145. package/node_modules/@freeharness/sdd/dist/src/shim/warnings.js +66 -0
  146. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.d.ts +4 -0
  147. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/line-endings.js +17 -0
  148. package/node_modules/@freeharness/sdd/dist/vendor/openspec/apply/specs-apply.js +95 -22
  149. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.d.ts +6 -0
  150. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/change-status-policy.js +14 -6
  151. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.d.ts +1 -1
  152. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/index.js +1 -1
  153. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.d.ts +3 -3
  154. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/outputs.js +24 -4
  155. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/task-progress.js +4 -7
  156. package/node_modules/@freeharness/sdd/dist/vendor/openspec/engine/types.js +13 -1
  157. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/apply-change.js +21 -6
  158. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/archive-change.js +80 -19
  159. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/explore.js +42 -22
  160. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.d.ts +32 -0
  161. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/project-root.js +39 -0
  162. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/propose.js +16 -9
  163. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/sync-specs.js +36 -1
  164. package/node_modules/@freeharness/sdd/dist/vendor/openspec/generation/templates/workflows/update-change.js +65 -30
  165. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.d.ts +1 -1
  166. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/constants.js +1 -1
  167. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.d.ts +8 -13
  168. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/change-parser.js +79 -102
  169. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/markdown-parser.js +4 -9
  170. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.d.ts +50 -10
  171. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-blocks.js +165 -37
  172. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.d.ts +24 -3
  173. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/requirement-text.js +55 -8
  174. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/parsers/spec-structure.js +4 -1
  175. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/purpose-placeholder.js +19 -9
  176. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.d.ts +10 -0
  177. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/spec-discovery.js +115 -0
  178. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.d.ts +3 -3
  179. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/task-lines.js +20 -20
  180. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.d.ts +6 -0
  181. package/node_modules/@freeharness/sdd/dist/vendor/openspec/validation/validator.js +75 -14
  182. package/node_modules/@freeharness/sdd/package.json +1 -1
  183. package/node_modules/@freeharness/sdd/vendor/openspec/assets/schema.upstream.yaml +47 -7
  184. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-apply-change/SKILL.md +25 -7
  185. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-archive-change/SKILL.md +50 -11
  186. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-bulk-archive-change/SKILL.md +48 -10
  187. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-continue-change/SKILL.md +27 -7
  188. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-explore/SKILL.md +25 -12
  189. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-ff-change/SKILL.md +17 -5
  190. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-new-change/SKILL.md +12 -1
  191. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-onboard/SKILL.md +47 -33
  192. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-propose/SKILL.md +17 -5
  193. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-sync-specs/SKILL.md +29 -1
  194. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-update-change/SKILL.md +31 -13
  195. package/node_modules/@freeharness/sdd/vendor/openspec/assets/skills/openspec-verify-change/SKILL.md +75 -27
  196. package/package.json +6 -4
  197. package/provenance/canonical.PROVENANCE.yaml +3 -3
  198. package/provenance/memory.PROVENANCE.yaml +472 -6
  199. package/provenance/sdd.PROVENANCE.yaml +279 -2
  200. package/canonical/hooks/freeharness-auto-memory.mjs +0 -158
  201. package/canonical/hooks/hooks.auto-memory.claude-code.json +0 -16
  202. package/canonical/openspec/config.base.yaml +0 -89
  203. package/canonical/rules/safety.md +0 -41
@@ -16,7 +16,7 @@ export declare const UPSTREAM_PLUGIN_KEY = "claude-mem@thedotmack";
16
16
  * canonicalDir: The resolved FreeHarness canonical directory (packages/cli/canonical).
17
17
  *
18
18
  * Returns:
19
- * Absolute engine path, or null when the vendored tree is absent (npx bridge).
19
+ * Absolute engine path, or null when the vendored tree is absent (memory is then skipped).
20
20
  */
21
21
  export declare function vendoredMemoryEngineDir(canonicalDir: string): string | null;
22
22
  /**
@@ -49,19 +49,44 @@ export interface ClaudePluginPaths {
49
49
  * The path set mirrored from upstream utils/paths.ts.
50
50
  */
51
51
  export declare function claudePluginPaths(version: string): ClaudePluginPaths;
52
+ /** Built bundles a plugin dir needs before it may replace a cached build. */
53
+ export declare const PLUGIN_BUNDLES: readonly ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
52
54
  /**
53
- * Content digest of a plugin bundle: every file's relative posix path and bytes, in
54
- * path order, so a same-version rebuild whose output is byte-identical digests the
55
- * same as the previous run. Skips `node_modules` (matches the copy filter) and the
56
- * marker file itself, so re-staging never invalidates its own guard.
55
+ * Digest of a plugin bundle over every entry's relpath, type, mode and content or link target,
56
+ * in path order. Walks with lstat and never follows a symlink. Skips `node_modules` and the
57
+ * unstaged entries, so the vendored plugin and its staged copy digest alike.
57
58
  *
58
59
  * Args:
59
60
  * dir: Directory to digest recursively (absent dir digests as empty).
60
61
  *
61
62
  * Returns:
62
- * Lowercase hex SHA-256 over `path + NUL + content + NUL` per file.
63
+ * Lowercase hex SHA-256.
63
64
  */
64
65
  export declare function bundleDigest(dir: string): string;
66
+ export interface StageResult {
67
+ staged: boolean;
68
+ digest: string;
69
+ notes: string[];
70
+ }
71
+ /**
72
+ * Stage a plugin dir into a cache dir unless the marker and the cache's own digest both equal
73
+ * the plugin digest and the declared deps resolve; `staged` is true for a (would-be) re-stage.
74
+ */
75
+ export declare function stagePluginCache(pluginDir: string, cacheDir: string, opts: {
76
+ dryRun: boolean;
77
+ }): StageResult;
78
+ /** Identity of the cache marker file (absent: null); every re-stage writes a new one. */
79
+ export declare function stageStamp(cacheDir: string): string | null;
80
+ export declare function missingPluginBundles(pluginDir: string): string[];
81
+ /** Whether a plugin dir's package.json declares runtime deps, which only Bun installs. */
82
+ export declare function declaresPluginDeps(dir: string): boolean;
83
+ /**
84
+ * Declared plugin deps that do not resolve from `<dir>/node_modules` (upstream
85
+ * verifyCriticalModules), plus the zod subpaths the worker requires when zod is declared.
86
+ */
87
+ export declare function missingPluginDeps(dir: string): string[];
88
+ /** Install a staged cache's runtime deps (the hooks run from there); throws when one stays missing. */
89
+ export declare function installPluginDeps(dir: string, bunPath: string): void;
65
90
  /** Probe result for one runtime tool. */
66
91
  export interface ToolProbe {
67
92
  path: string | null;
@@ -74,31 +99,39 @@ export interface ToolProbe {
74
99
  * Path + version, or nulls when absent.
75
100
  */
76
101
  export declare function probeBun(): ToolProbe;
77
- /**
78
- * Probe uv (Chroma vector store runtime; upstream setup-runtime getUvPath).
79
- *
80
- * Returns:
81
- * Path + version, or nulls when absent.
82
- */
83
- export declare function probeUv(): ToolProbe;
84
102
  /**
85
103
  * Whether an upstream registration (UPSTREAM_PLUGIN_KEY) is still live: present in
86
- * installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Once a
87
- * migration sets it to `false`, later `registerHooks` runs see it as already migrated and skip
88
- * the backup/disable branch (idempotent on re-run).
104
+ * installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Read-only:
105
+ * doctor warns on it and the data copy refuses while it is live.
89
106
  *
90
107
  * Args:
91
108
  * installed: Parsed installed_plugins.json (or `{}` when absent).
92
109
  * settings: Parsed settings.json (or `{}` when absent).
93
110
  *
94
111
  * Returns:
95
- * True when the upstream key still needs disabling.
112
+ * True when the upstream plugin may still load beside free-mem.
96
113
  */
97
114
  export declare function isUpstreamRegistered(installed: {
98
115
  plugins?: Record<string, unknown>;
99
116
  }, settings: {
100
117
  enabledPlugins?: Record<string, boolean>;
101
118
  }): boolean;
119
+ /** Write the `.install-version` marker that tells our build apart; never writes through a symlink. */
120
+ export declare function writeInstallMarker(dir: string, version: string, bun: string): string;
121
+ /**
122
+ * Version from a plugin dir's manifest ("0.0.0" when absent). The version names a cache
123
+ * directory that staging removes, so an empty or path-like value must never reach a path.
124
+ *
125
+ * Args:
126
+ * pluginDir: Plugin directory holding `.claude-plugin/plugin.json`.
127
+ *
128
+ * Returns:
129
+ * The semver version.
130
+ *
131
+ * Raises:
132
+ * Error: When the manifest version is not a semver version.
133
+ */
134
+ export declare function pluginVersion(pluginDir: string): string;
102
135
  /** MemoryPort implementation backed by the vendored claude-mem plugin build. */
103
136
  export declare class VendoredMemory implements MemoryPort {
104
137
  readonly engineDir: string;
@@ -115,7 +148,7 @@ export declare class VendoredMemory implements MemoryPort {
115
148
  version(): string;
116
149
  private result;
117
150
  /**
118
- * Probe Bun and uv; instruct when missing. Never installs them (user's machine).
151
+ * Probe Bun; instruct when missing. Never installs it (user's machine).
119
152
  *
120
153
  * Returns:
121
154
  * Notes only. A missing Bun is reported as "MISSING" and blocks install().
@@ -131,20 +164,9 @@ export declare class VendoredMemory implements MemoryPort {
131
164
  * Register the plugin with Claude Code (upstream registerMarketplace / registerPlugin /
132
165
  * enablePluginInClaudeSettings). All three writes are additive merges: other
133
166
  * marketplaces, plugins, hooks and settings keys are preserved byte-for-byte in shape.
134
- *
135
- * When an upstream registration (UPSTREAM_PLUGIN_KEY) is found (installed_plugins.json
136
- * or a still-true enabledPlugins entry), this also backs up the three files below with a
137
- * shared timestamp before the first write and disables (never deletes) the old key; the
138
- * old cache directory is left in place for `freeharness cleanup` to offer later.
167
+ * An upstream registration (UPSTREAM_PLUGIN_KEY) is never written: doctor warns about it.
139
168
  */
140
169
  registerHooks(_scope: Scope, ctx: InstallContext): Promise<AdapterResult>;
141
- /**
142
- * Keep an existing ~/.claude-mem data directory from a delegated install. The
143
- * plugin resolves it by convention (CLAUDE_MEM_DATA_DIR or ~/.claude-mem), so no
144
- * write is needed and nothing is deleted.
145
- */
146
170
  migrateExisting(_ctx: InstallContext): Promise<AdapterResult>;
147
171
  private run;
148
- private missingPluginDeps;
149
- private verifyPluginDeps;
150
172
  }
@@ -1,26 +1,17 @@
1
1
  // MemoryPort adapter for the vendored claude-mem engine (FreeHarness V3-P0 memory-runtime).
2
2
  //
3
- // Installs free-mem as a real Claude Code plugin from OUR build, reproducing the
4
- // upstream `npx claude-mem install` mechanics (src/npx-cli/commands/install.ts +
5
- // utils/paths.ts + install/setup-runtime.ts) in TypeScript:
3
+ // Installs free-mem as a real Claude Code plugin from OUR build, reproducing the upstream `npx claude-mem
4
+ // install` mechanics (src/npx-cli/commands/install.ts + utils/paths.ts + install/setup-runtime.ts) in TypeScript:
6
5
  //
7
- // ensurePrerequisites Bun (build + hook runtime) and uv (Chroma) probes.
8
- // install bun run build:plugin -> copyPluginToCache(version)
9
- // -> copyPluginToMarketplace -> plugin deps install
10
- // -> .install-version markers.
11
- // registerHooks known_marketplaces.json + installed_plugins.json +
12
- // settings.enabledPlugins["free-mem@freeharness"]=true,
13
- // all ADDITIVE merges (other entries survive). When an
14
- // upstream registration (UPSTREAM_PLUGIN_KEY) is found, it
15
- // also backs up the three registration files, registers the
16
- // new key and disables (never deletes) the old one.
17
- // migrateExisting an existing ~/.claude-mem data dir is kept; the plugin
18
- // already resolves it by convention (nothing is deleted).
6
+ // ensurePrerequisites Bun probe (build + hook runtime), the only prerequisite.
7
+ // install bun run build:plugin -> copyPluginToCache(version) -> copyPluginToMarketplace
8
+ // -> plugin deps install -> .install-version markers.
9
+ // registerHooks known_marketplaces.json + installed_plugins.json + enabledPlugins
10
+ // ["free-mem@freeharness"], all ADDITIVE merges; the upstream key
11
+ // (UPSTREAM_PLUGIN_KEY) is never written; doctor warns while it is live.
19
12
  //
20
- // The FreeHarness build declares its own identity (marketplace freeharness, plugin
21
- // free-mem) so `doctor` and `cleanup` can tell it apart from an upstream or
22
- // pre-rename install; the data directory stays ~/.claude-mem by convention on
23
- // both sides. Every write honors ctx.dryRun (writes nothing, reports the plan).
13
+ // The FreeHarness build declares its own identity (marketplace freeharness, plugin free-mem) so `doctor` and
14
+ // `cleanup` can tell it apart from an upstream or pre-rename install. Every write honors ctx.dryRun.
24
15
  import crypto from "node:crypto";
25
16
  import fs from "node:fs";
26
17
  import os from "node:os";
@@ -45,7 +36,7 @@ const CAPABILITY_ID = "memory";
45
36
  * canonicalDir: The resolved FreeHarness canonical directory (packages/cli/canonical).
46
37
  *
47
38
  * Returns:
48
- * Absolute engine path, or null when the vendored tree is absent (npx bridge).
39
+ * Absolute engine path, or null when the vendored tree is absent (memory is then skipped).
49
40
  */
50
41
  export function vendoredMemoryEngineDir(canonicalDir) {
51
42
  const engineDir = path.join(canonicalDir, "..", "..", "memory", "vendor", UPSTREAM_PLUGIN_NAME, "engine");
@@ -63,7 +54,7 @@ export function vendoredMemoryEngineDir(canonicalDir) {
63
54
  export function memoryBuildActions(engineDir) {
64
55
  return [
65
56
  {
66
- description: "Build the vendored claude-mem plugin bundles (worker, mcp, context, watcher) with Bun",
57
+ description: "Build the free-mem plugin bundles (worker, mcp, context, watcher) from the vendored engine with Bun",
67
58
  command: "bun install && bun run build:plugin",
68
59
  cwd: engineDir,
69
60
  optional: true,
@@ -93,42 +84,127 @@ export function claudePluginPaths(version) {
93
84
  };
94
85
  }
95
86
  /**
96
- * Content digest of a plugin bundle: every file's relative posix path and bytes, in
97
- * path order, so a same-version rebuild whose output is byte-identical digests the
98
- * same as the previous run. Skips `node_modules` (matches the copy filter) and the
99
- * marker file itself, so re-staging never invalidates its own guard.
87
+ * Top-level entries the cache gets after the copy (our markers, the deps lockfile, Claude
88
+ * Code's in-use and orphan markers): never copied and never digested, so staging cannot write
89
+ * through a plugin symlink and an install never reads its own bookkeeping as tampering.
90
+ */
91
+ const UNSTAGED_ENTRIES = new Set([BUNDLE_MARKER_NAME, ".install-version", "bun.lock", "bun.lockb", ".in_use", ".orphaned_at"]);
92
+ const S_IFMT = 0o170000;
93
+ /** Built bundles a plugin dir needs before it may replace a cached build. */
94
+ export const PLUGIN_BUNDLES = ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
95
+ /**
96
+ * Digest of a plugin bundle over every entry's relpath, type, mode and content or link target,
97
+ * in path order. Walks with lstat and never follows a symlink. Skips `node_modules` and the
98
+ * unstaged entries, so the vendored plugin and its staged copy digest alike.
100
99
  *
101
100
  * Args:
102
101
  * dir: Directory to digest recursively (absent dir digests as empty).
103
102
  *
104
103
  * Returns:
105
- * Lowercase hex SHA-256 over `path + NUL + content + NUL` per file.
104
+ * Lowercase hex SHA-256.
106
105
  */
107
106
  export function bundleDigest(dir) {
108
- const files = [];
107
+ const entries = [];
109
108
  const walk = (rel) => {
110
- for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
111
- if (entry.name === "node_modules" || entry.name === BUNDLE_MARKER_NAME)
109
+ for (const name of fs.readdirSync(path.join(dir, rel))) {
110
+ if (name === "node_modules" || (!rel && UNSTAGED_ENTRIES.has(name)))
112
111
  continue;
113
- const childRel = rel ? `${rel}/${entry.name}` : entry.name;
114
- if (entry.isDirectory())
112
+ const childRel = rel ? `${rel}/${name}` : name;
113
+ const abs = path.join(dir, childRel);
114
+ const stat = fs.lstatSync(abs);
115
+ if (!stat.isFile() && !stat.isDirectory() && !stat.isSymbolicLink())
116
+ continue;
117
+ entries.push({ rel: childRel, abs, stat });
118
+ if (stat.isDirectory())
115
119
  walk(childRel);
116
- else if (entry.isFile())
117
- files.push(childRel);
118
120
  }
119
121
  };
120
122
  if (fs.existsSync(dir))
121
123
  walk("");
122
- files.sort();
124
+ entries.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0));
123
125
  const hash = crypto.createHash("sha256");
124
- for (const rel of files) {
125
- hash.update(rel, "utf-8");
126
- hash.update("\0");
127
- hash.update(fs.readFileSync(path.join(dir, rel)));
126
+ for (const { rel, abs, stat } of entries) {
127
+ const type = stat.isSymbolicLink() ? "l" : stat.isDirectory() ? "d" : "f";
128
+ // Windows has no exec bits and symlink permissions carry no meaning.
129
+ const mode = IS_WINDOWS || type === "l" ? stat.mode & S_IFMT : stat.mode;
130
+ for (const part of [rel, type, mode.toString(8)]) {
131
+ hash.update(part, "utf-8");
132
+ hash.update("\0");
133
+ }
134
+ if (type === "f")
135
+ hash.update(fs.readFileSync(abs));
136
+ else if (type === "l")
137
+ hash.update(fs.readlinkSync(abs), "utf-8");
128
138
  hash.update("\0");
129
139
  }
130
140
  return hash.digest("hex");
131
141
  }
142
+ /**
143
+ * Stage a plugin dir into a cache dir unless the marker and the cache's own digest both equal
144
+ * the plugin digest and the declared deps resolve; `staged` is true for a (would-be) re-stage.
145
+ */
146
+ export function stagePluginCache(pluginDir, cacheDir, opts) {
147
+ const digest = bundleDigest(pluginDir);
148
+ const short = digest.slice(0, 8);
149
+ const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
150
+ const marker = fs.existsSync(markerFile) ? fs.readFileSync(markerFile, "utf-8").trim() : null;
151
+ if (marker === digest && bundleDigest(cacheDir) === marker && missingPluginDeps(cacheDir).length === 0) {
152
+ return { staged: false, digest, notes: [`plugin cache up to date (bundle ${short})`] };
153
+ }
154
+ if (opts.dryRun)
155
+ return { staged: true, digest, notes: [`Would re-stage the plugin cache at ${cacheDir} (bundle ${short}).`] };
156
+ fs.rmSync(cacheDir, { recursive: true, force: true });
157
+ fs.mkdirSync(cacheDir, { recursive: true });
158
+ const root = path.resolve(pluginDir);
159
+ const unstaged = (src) => path.basename(src) === "node_modules" || (path.resolve(path.dirname(src)) === root && UNSTAGED_ENTRIES.has(path.basename(src)));
160
+ fs.cpSync(pluginDir, cacheDir, { recursive: true, force: true, verbatimSymlinks: true, filter: (src) => !unstaged(src) });
161
+ fs.writeFileSync(markerFile, `${digest}\n`, { encoding: "utf-8", flag: "wx" });
162
+ return { staged: true, digest, notes: [`Re-staged the plugin cache at ${cacheDir} (bundle ${short}).`] };
163
+ }
164
+ /** Identity of the cache marker file (absent: null); every re-stage writes a new one. */
165
+ export function stageStamp(cacheDir) {
166
+ const markerFile = path.join(cacheDir, BUNDLE_MARKER_NAME);
167
+ if (!fs.existsSync(markerFile))
168
+ return null;
169
+ const st = fs.lstatSync(markerFile);
170
+ return `${st.ino}:${st.mtimeMs}:${fs.readFileSync(markerFile, "utf-8").trim()}`;
171
+ }
172
+ export function missingPluginBundles(pluginDir) {
173
+ return PLUGIN_BUNDLES.filter((f) => !fs.existsSync(path.join(pluginDir, "scripts", f)));
174
+ }
175
+ /** Whether a plugin dir's package.json declares runtime deps, which only Bun installs. */
176
+ export function declaresPluginDeps(dir) {
177
+ const pkg = readJsonSafe(path.join(dir, "package.json"), {});
178
+ return Object.keys(pkg.dependencies ?? {}).length > 0;
179
+ }
180
+ /**
181
+ * Declared plugin deps that do not resolve from `<dir>/node_modules` (upstream
182
+ * verifyCriticalModules), plus the zod subpaths the worker requires when zod is declared.
183
+ */
184
+ export function missingPluginDeps(dir) {
185
+ const pkg = readJsonSafe(path.join(dir, "package.json"), {});
186
+ const deps = Object.keys(pkg.dependencies ?? {});
187
+ const nm = path.join(dir, "node_modules");
188
+ const missing = deps.filter((dep) => !fs.existsSync(path.join(nm, dep, "package.json")));
189
+ if (deps.includes("zod")) {
190
+ for (const sub of ["v3", "v4", "v4-mini"]) {
191
+ if (!fs.existsSync(path.join(nm, "zod", sub)))
192
+ missing.push(`zod/${sub}`);
193
+ }
194
+ }
195
+ return missing;
196
+ }
197
+ /** Install a staged cache's runtime deps (the hooks run from there); throws when one stays missing. */
198
+ export function installPluginDeps(dir, bunPath) {
199
+ const r = spawnSync(bunPath, ["install", "--ignore-scripts"], { cwd: dir, stdio: "inherit", windowsHide: true, env: process.env });
200
+ if (r.error)
201
+ throw new Error(`bun install (plugin deps) failed: ${r.error.message}`);
202
+ if (r.status !== 0)
203
+ throw new Error(`bun install (plugin deps) failed (exit ${r.status})`);
204
+ const missing = missingPluginDeps(dir);
205
+ if (missing.length > 0)
206
+ throw new Error(`plugin deps missing after install in ${dir}: ${missing.join(", ")}`);
207
+ }
132
208
  function probeTool(command, commonPaths) {
133
209
  const candidates = [command, ...commonPaths];
134
210
  for (const candidate of candidates) {
@@ -137,7 +213,7 @@ function probeTool(command, commonPaths) {
137
213
  try {
138
214
  const r = spawnSync(candidate, ["--version"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
139
215
  if (r.status === 0) {
140
- // "1.3.14" (bun) or "uv 0.9.2 (hash date)" -> first semver-looking token.
216
+ // "1.3.14" or "tool 0.9.2 (hash date)" -> first semver-looking token.
141
217
  const semver = r.stdout.match(/\d+\.\d+(?:\.\d+)?/);
142
218
  return { path: candidate, version: semver ? semver[0] : r.stdout.trim() };
143
219
  }
@@ -161,31 +237,17 @@ export function probeBun() {
161
237
  : [path.join(home, ".bun", "bin", "bun"), "/usr/local/bin/bun", "/opt/homebrew/bin/bun", "/home/linuxbrew/.linuxbrew/bin/bun"];
162
238
  return probeTool("bun", common);
163
239
  }
164
- /**
165
- * Probe uv (Chroma vector store runtime; upstream setup-runtime getUvPath).
166
- *
167
- * Returns:
168
- * Path + version, or nulls when absent.
169
- */
170
- export function probeUv() {
171
- const home = os.homedir();
172
- const common = IS_WINDOWS
173
- ? [path.join(home, ".local", "bin", "uv.exe"), path.join(home, ".cargo", "bin", "uv.exe")]
174
- : [path.join(home, ".local", "bin", "uv"), path.join(home, ".cargo", "bin", "uv"), "/usr/local/bin/uv", "/opt/homebrew/bin/uv"];
175
- return probeTool("uv", common);
176
- }
177
240
  /**
178
241
  * Whether an upstream registration (UPSTREAM_PLUGIN_KEY) is still live: present in
179
- * installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Once a
180
- * migration sets it to `false`, later `registerHooks` runs see it as already migrated and skip
181
- * the backup/disable branch (idempotent on re-run).
242
+ * installed_plugins.json, or enabledPlugins carries it at a value other than `false`. Read-only:
243
+ * doctor warns on it and the data copy refuses while it is live.
182
244
  *
183
245
  * Args:
184
246
  * installed: Parsed installed_plugins.json (or `{}` when absent).
185
247
  * settings: Parsed settings.json (or `{}` when absent).
186
248
  *
187
249
  * Returns:
188
- * True when the upstream key still needs disabling.
250
+ * True when the upstream plugin may still load beside free-mem.
189
251
  */
190
252
  export function isUpstreamRegistered(installed, settings) {
191
253
  if (settings.enabledPlugins?.[UPSTREAM_PLUGIN_KEY] === false)
@@ -212,12 +274,36 @@ function writeJsonAtomic(file, value) {
212
274
  fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", "utf-8");
213
275
  fs.renameSync(tmp, file);
214
276
  }
215
- function writeInstallMarker(dir, version, bun, uv) {
277
+ /** Write the `.install-version` marker that tells our build apart; never writes through a symlink. */
278
+ export function writeInstallMarker(dir, version, bun) {
216
279
  const marker = path.join(dir, ".install-version");
217
280
  fs.mkdirSync(dir, { recursive: true });
218
- fs.writeFileSync(marker, JSON.stringify({ version, bun, uv, installedAt: new Date().toISOString() }));
281
+ fs.rmSync(marker, { force: true });
282
+ fs.writeFileSync(marker, JSON.stringify({ version, bun, uv: "unused", installedAt: new Date().toISOString() }), { flag: "wx" });
219
283
  return marker;
220
284
  }
285
+ const PLUGIN_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
286
+ /**
287
+ * Version from a plugin dir's manifest ("0.0.0" when absent). The version names a cache
288
+ * directory that staging removes, so an empty or path-like value must never reach a path.
289
+ *
290
+ * Args:
291
+ * pluginDir: Plugin directory holding `.claude-plugin/plugin.json`.
292
+ *
293
+ * Returns:
294
+ * The semver version.
295
+ *
296
+ * Raises:
297
+ * Error: When the manifest version is not a semver version.
298
+ */
299
+ export function pluginVersion(pluginDir) {
300
+ const manifest = path.join(pluginDir, ".claude-plugin", "plugin.json");
301
+ const version = readJsonSafe(manifest, {}).version ?? "0.0.0";
302
+ if (typeof version !== "string" || !PLUGIN_SEMVER.test(version)) {
303
+ throw new Error(`plugin version ${JSON.stringify(version)} in ${manifest} is not a semver version; refusing to stage the plugin cache`);
304
+ }
305
+ return version;
306
+ }
221
307
  /** MemoryPort implementation backed by the vendored claude-mem plugin build. */
222
308
  export class VendoredMemory {
223
309
  engineDir;
@@ -237,15 +323,13 @@ export class VendoredMemory {
237
323
  }
238
324
  /** Plugin version from the vendored manifest (upstream readPluginVersion). */
239
325
  version() {
240
- const manifest = path.join(this.pluginDir, ".claude-plugin", "plugin.json");
241
- const parsed = readJsonSafe(manifest, {});
242
- return parsed.version ?? "0.0.0";
326
+ return pluginVersion(this.pluginDir);
243
327
  }
244
328
  result() {
245
329
  return { capabilityId: CAPABILITY_ID, target: this.target, written: [], actions: [], notes: [] };
246
330
  }
247
331
  /**
248
- * Probe Bun and uv; instruct when missing. Never installs them (user's machine).
332
+ * Probe Bun; instruct when missing. Never installs it (user's machine).
249
333
  *
250
334
  * Returns:
251
335
  * Notes only. A missing Bun is reported as "MISSING" and blocks install().
@@ -253,11 +337,7 @@ export class VendoredMemory {
253
337
  async ensurePrerequisites(_ctx) {
254
338
  const res = this.result();
255
339
  const bun = probeBun();
256
- const uv = probeUv();
257
340
  res.notes.push(bun.path ? `Bun ${bun.version} found (${bun.path}).` : "MISSING: Bun — install from https://bun.sh (curl -fsSL https://bun.sh/install | bash), then re-run.");
258
- res.notes.push(uv.path
259
- ? `uv ${uv.version} found (${uv.path}).`
260
- : "MISSING: uv — Chroma vector search needs it at runtime (https://docs.astral.sh/uv). Install is not blocked; recall degrades to SQLite-only until uv is present.");
261
341
  return res;
262
342
  }
263
343
  /**
@@ -274,7 +354,6 @@ export class VendoredMemory {
274
354
  res.notes.push("Install skipped: Bun is required to build the plugin and to run its hooks.");
275
355
  return res;
276
356
  }
277
- const uv = probeUv();
278
357
  const scriptsDir = path.join(this.pluginDir, "scripts");
279
358
  const marketplacePlugin = path.join(P.marketplaceDir, "plugin");
280
359
  // 1. Build (bun install once, then build:plugin).
@@ -282,8 +361,7 @@ export class VendoredMemory {
282
361
  res.notes.push(`Would build the plugin bundles: bun run build:plugin (in ${this.engineDir}).`);
283
362
  }
284
363
  else {
285
- const bundles = ["bun-runner.js", "worker-service.cjs", "mcp-server.cjs"];
286
- const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && bundles.every((f) => fs.existsSync(path.join(scriptsDir, f)));
364
+ const reuse = process.env.FREEHARNESS_MEMORY_SKIP_BUILD === "1" && missingPluginBundles(this.pluginDir).length === 0;
287
365
  if (reuse) {
288
366
  res.notes.push(`Reused the built plugin bundles in ${scriptsDir} (FREEHARNESS_MEMORY_SKIP_BUILD=1).`);
289
367
  }
@@ -291,34 +369,17 @@ export class VendoredMemory {
291
369
  if (!fs.existsSync(path.join(this.engineDir, "node_modules")))
292
370
  this.run(bun.path, ["install"], this.engineDir, "bun install (engine)");
293
371
  this.run(bun.path, ["run", "build:plugin"], this.engineDir, "bun run build:plugin");
294
- for (const f of bundles) {
295
- if (!fs.existsSync(path.join(scriptsDir, f)))
296
- throw new Error(`build:plugin did not produce ${f}`);
297
- }
372
+ for (const f of missingPluginBundles(this.pluginDir))
373
+ throw new Error(`build:plugin did not produce ${f}`);
298
374
  res.notes.push(`Built plugin bundles v${version} into ${scriptsDir}.`);
299
375
  }
300
376
  }
301
- // 2. copyPluginToCache: rm + cp plugin/ -> cache/<owner>/<name>/<version>/, skipped
302
- // when the cache already holds this exact bundle (bundle.sha256 marker matches) AND
303
- // its runtime deps still resolve; a matching marker over a broken or partial
304
- // node_modules takes the full re-stage path instead of being reported healthy.
305
- const digest = bundleDigest(this.pluginDir);
306
- const markerFile = path.join(P.cacheDir, BUNDLE_MARKER_NAME);
307
- const markerMatches = fs.existsSync(P.cacheDir) && fs.existsSync(markerFile) && fs.readFileSync(markerFile, "utf-8").trim() === digest;
308
- const cacheUpToDate = markerMatches && fs.existsSync(path.join(P.cacheDir, "node_modules")) && this.missingPluginDeps(P.cacheDir).length === 0;
377
+ // 2. copyPluginToCache: rm + cp plugin/ -> cache/<owner>/<name>/<version>/, skipped when
378
+ // the cache already holds this exact bundle; a broken node_modules takes the re-stage path.
379
+ const stage = stagePluginCache(this.pluginDir, P.cacheDir, { dryRun: ctx.dryRun });
380
+ const cacheUpToDate = !stage.staged;
309
381
  res.written.push(P.cacheDir);
310
- if (cacheUpToDate) {
311
- res.notes.push(`plugin cache up to date (bundle ${digest.slice(0, 8)})`);
312
- }
313
- else if (ctx.dryRun) {
314
- res.notes.push(`Would re-stage the plugin cache at ${P.cacheDir} (bundle ${digest.slice(0, 8)}).`);
315
- }
316
- else {
317
- fs.rmSync(P.cacheDir, { recursive: true, force: true });
318
- fs.mkdirSync(P.cacheDir, { recursive: true });
319
- fs.cpSync(this.pluginDir, P.cacheDir, { recursive: true, force: true, filter: (src) => path.basename(src) !== "node_modules" });
320
- fs.writeFileSync(markerFile, `${digest}\n`, "utf-8");
321
- }
382
+ res.notes.push(...stage.notes);
322
383
  // 3. copyPluginToMarketplace: plugin/ + marketplace manifest + package bits.
323
384
  res.written.push(marketplacePlugin);
324
385
  if (!ctx.dryRun) {
@@ -343,8 +404,7 @@ export class VendoredMemory {
343
404
  res.notes.push(`Would install plugin runtime deps in ${P.cacheDir} (bun install --ignore-scripts).`);
344
405
  }
345
406
  else {
346
- this.run(bun.path, ["install", "--ignore-scripts"], P.cacheDir, "bun install (plugin deps)");
347
- this.verifyPluginDeps(P.cacheDir);
407
+ installPluginDeps(P.cacheDir, bun.path);
348
408
  res.notes.push("Plugin runtime deps installed (zod resolvable from plugin/node_modules).");
349
409
  }
350
410
  // 5. Install markers (cache + marketplace + marketplace/plugin, as upstream); the
@@ -353,11 +413,11 @@ export class VendoredMemory {
353
413
  for (const dir of [P.marketplaceDir, marketplacePlugin]) {
354
414
  res.written.push(path.join(dir, ".install-version"));
355
415
  if (!ctx.dryRun)
356
- writeInstallMarker(dir, version, bun.version ?? "unknown", uv.version ?? "unknown");
416
+ writeInstallMarker(dir, version, bun.version ?? "unknown");
357
417
  }
358
418
  res.written.push(path.join(P.cacheDir, ".install-version"));
359
419
  if (!cacheUpToDate && !ctx.dryRun)
360
- writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown", uv.version ?? "unknown");
420
+ writeInstallMarker(P.cacheDir, version, bun.version ?? "unknown");
361
421
  res.notes.push(`Plugin v${version} staged at ${P.cacheDir} (marketplace: ${P.marketplaceDir}).`);
362
422
  return res;
363
423
  }
@@ -365,30 +425,13 @@ export class VendoredMemory {
365
425
  * Register the plugin with Claude Code (upstream registerMarketplace / registerPlugin /
366
426
  * enablePluginInClaudeSettings). All three writes are additive merges: other
367
427
  * marketplaces, plugins, hooks and settings keys are preserved byte-for-byte in shape.
368
- *
369
- * When an upstream registration (UPSTREAM_PLUGIN_KEY) is found (installed_plugins.json
370
- * or a still-true enabledPlugins entry), this also backs up the three files below with a
371
- * shared timestamp before the first write and disables (never deletes) the old key; the
372
- * old cache directory is left in place for `freeharness cleanup` to offer later.
428
+ * An upstream registration (UPSTREAM_PLUGIN_KEY) is never written: doctor warns about it.
373
429
  */
374
430
  async registerHooks(_scope, ctx) {
375
431
  const res = this.result();
376
432
  const version = this.version();
377
433
  const P = claudePluginPaths(version);
378
434
  const now = new Date().toISOString();
379
- const installedBefore = readJsonSafe(P.installedPlugins, {});
380
- const settingsBefore = readJsonSafe(P.settings, {});
381
- const migrating = isUpstreamRegistered(installedBefore, settingsBefore);
382
- if (migrating && !ctx.dryRun) {
383
- const stamp = now.replace(/[:.]/g, "-");
384
- for (const f of [P.installedPlugins, P.knownMarketplaces, P.settings]) {
385
- if (!fs.existsSync(f))
386
- continue;
387
- const backup = `${f}.${stamp}.bak`;
388
- fs.copyFileSync(f, backup);
389
- res.written.push(backup);
390
- }
391
- }
392
435
  // known_marketplaces.json: { "<owner>": { source, installLocation, lastUpdated, autoUpdate } }
393
436
  const known = readJsonSafe(P.knownMarketplaces, {});
394
437
  known[MARKETPLACE_OWNER] = {
@@ -401,8 +444,6 @@ export class VendoredMemory {
401
444
  if (!ctx.dryRun)
402
445
  writeJsonAtomic(P.knownMarketplaces, known);
403
446
  // installed_plugins.json: { version: 2, plugins: { "<name>@<owner>": [ {scope, installPath, version, installedAt, lastUpdated} ] } }
404
- // The upstream entry (if any) is never removed here: invariant 3 forbids deleting a
405
- // foreign key, and the cache directory it points at stays reachable for a rollback.
406
447
  const installed = readJsonSafe(P.installedPlugins, {});
407
448
  if (!installed.version)
408
449
  installed.version = 2;
@@ -412,49 +453,21 @@ export class VendoredMemory {
412
453
  res.written.push(P.installedPlugins);
413
454
  if (!ctx.dryRun)
414
455
  writeJsonAtomic(P.installedPlugins, installed);
415
- // settings.json: enabledPlugins[PLUGIN_KEY] = true (everything else kept); migrating
416
- // also sets enabledPlugins[UPSTREAM_PLUGIN_KEY] = false so Claude Code stops loading it.
456
+ // settings.json: enabledPlugins[PLUGIN_KEY] = true (everything else kept).
417
457
  const settings = readJsonSafe(P.settings, {});
418
458
  if (!settings.enabledPlugins)
419
459
  settings.enabledPlugins = {};
420
460
  const already = settings.enabledPlugins[PLUGIN_KEY] === true;
421
461
  settings.enabledPlugins[PLUGIN_KEY] = true;
422
- if (migrating)
423
- settings.enabledPlugins[UPSTREAM_PLUGIN_KEY] = false;
424
462
  res.written.push(P.settings);
425
463
  if (!ctx.dryRun)
426
464
  writeJsonAtomic(P.settings, settings);
427
- if (migrating) {
428
- res.notes.push(ctx.dryRun
429
- ? `Would migrate from ${UPSTREAM_PLUGIN_KEY}: back up installed_plugins.json, known_marketplaces.json and settings.json before writing, register ${PLUGIN_KEY} v${version}, and set enabledPlugins["${UPSTREAM_PLUGIN_KEY}"] to false. The old cache directory is left in place.`
430
- : `Migrated from ${UPSTREAM_PLUGIN_KEY}: backed up installed_plugins.json, known_marketplaces.json and settings.json before the first write, registered ${PLUGIN_KEY} v${version}, and set enabledPlugins["${UPSTREAM_PLUGIN_KEY}"] to false. The old cache directory is left in place.`);
431
- }
432
- else {
433
- res.notes.push(`${ctx.dryRun ? "Would register" : "Registered"} plugin ${PLUGIN_KEY} v${version}: marketplace + installed_plugins + enabledPlugins (additive)${already ? "; was already enabled" : ""}.`);
434
- }
465
+ res.notes.push(`${ctx.dryRun ? "Would register" : "Registered"} plugin ${PLUGIN_KEY} v${version}: marketplace + installed_plugins + enabledPlugins (additive)${already ? "; was already enabled" : ""}.`);
435
466
  res.notes.push("Lifecycle hooks and the mem-search MCP come from the plugin's own hooks.json / .mcp.json (no settings.json hook entries are written).");
436
467
  return res;
437
468
  }
438
- /**
439
- * Keep an existing ~/.claude-mem data directory from a delegated install. The
440
- * plugin resolves it by convention (CLAUDE_MEM_DATA_DIR or ~/.claude-mem), so no
441
- * write is needed and nothing is deleted.
442
- */
443
469
  async migrateExisting(_ctx) {
444
- const res = this.result();
445
- const dataDir = process.env.CLAUDE_MEM_DATA_DIR || path.join(os.homedir(), ".claude-mem");
446
- if (!fs.existsSync(dataDir)) {
447
- res.notes.push(`No existing claude-mem data dir at ${dataDir}; the worker creates it on first start.`);
448
- return res;
449
- }
450
- const db = path.join(dataDir, "claude-mem.db");
451
- const settings = readJsonSafe(path.join(dataDir, "settings.json"), {});
452
- const redirected = typeof settings.CLAUDE_MEM_DATA_DIR === "string" ? settings.CLAUDE_MEM_DATA_DIR : null;
453
- res.notes.push(`Existing claude-mem data kept at ${dataDir}${fs.existsSync(db) ? " (claude-mem.db present)" : ""}; the vendored plugin reads it by convention${redirected ? ` (settings redirect data to ${redirected})` : ""}. Nothing deleted.`);
454
- const legacyMarker = path.join(dataDir, ".install-version");
455
- if (fs.existsSync(legacyMarker))
456
- res.notes.push("A legacy install marker exists in the data dir; the plugin markers now live in the plugin cache.");
457
- return res;
470
+ return this.result();
458
471
  }
459
472
  run(cmd, args, cwd, label) {
460
473
  const r = spawnSync(cmd, args, { cwd, stdio: "inherit", windowsHide: true, env: process.env });
@@ -463,25 +476,4 @@ export class VendoredMemory {
463
476
  if (r.status !== 0)
464
477
  throw new Error(`${label} failed (exit ${r.status})`);
465
478
  }
466
- // upstream verifyCriticalModules: every declared plugin dep must resolve from
467
- // plugin/node_modules, plus the zod subpaths the worker bundle requires.
468
- missingPluginDeps(dir) {
469
- const pkg = readJsonSafe(path.join(dir, "package.json"), {});
470
- const nm = path.join(dir, "node_modules");
471
- const missing = [];
472
- for (const dep of Object.keys(pkg.dependencies ?? {})) {
473
- if (!fs.existsSync(path.join(nm, dep, "package.json")))
474
- missing.push(dep);
475
- }
476
- for (const sub of ["v3", "v4", "v4-mini"]) {
477
- if (!fs.existsSync(path.join(nm, "zod", sub)))
478
- missing.push(`zod/${sub}`);
479
- }
480
- return missing;
481
- }
482
- verifyPluginDeps(dir) {
483
- const missing = this.missingPluginDeps(dir);
484
- if (missing.length > 0)
485
- throw new Error(`plugin deps missing after install in ${dir}: ${missing.join(", ")}`);
486
- }
487
479
  }