@phren/cli 0.0.1

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,591 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { execFileSync } from "child_process";
4
+ import { debugLog, EXEC_TIMEOUT_QUICK_MS, getProjectDirs, isRecord, homeDir, homePath, hookConfigPath, runtimeHealthFile, } from "./shared.js";
5
+ import { validateGovernanceJson } from "./shared-governance.js";
6
+ import { errorMessage } from "./utils.js";
7
+ import { buildIndex, queryRows } from "./shared-index.js";
8
+ import { validateTaskFormat, validateFindingsFormat } from "./shared-content.js";
9
+ import { detectInstalledTools } from "./hooks.js";
10
+ import { validateSkillFrontmatter, validateSkillsDir } from "./link-skills.js";
11
+ import { verifyFileChecksums, updateFileChecksums } from "./link-checksums.js";
12
+ import { buildSkillManifest } from "./skill-registry.js";
13
+ import { inspectTaskHygiene } from "./task-hygiene.js";
14
+ import { resolveTaskFilePath, TASK_FILE_ALIASES } from "./data-tasks.js";
15
+ import { repairPreexistingInstall } from "./init-setup.js";
16
+ import { getMachineName, lookupProfile, findProfileFile, getProfileProjects, findProjectDir, } from "./link.js";
17
+ import { claudeProjectKey } from "./link-context.js";
18
+ import { getProjectOwnershipMode, readProjectConfig } from "./project-config.js";
19
+ // ── Doctor ──────────────────────────────────────────────────────────────────
20
+ function isWrapperActive(tool) {
21
+ const wrapperPath = homePath(".local", "bin", tool);
22
+ if (!fs.existsSync(wrapperPath))
23
+ return false;
24
+ try {
25
+ const resolved = execFileSync("which", [tool], {
26
+ encoding: "utf8",
27
+ stdio: ["ignore", "pipe", "ignore"],
28
+ timeout: EXEC_TIMEOUT_QUICK_MS,
29
+ }).trim();
30
+ return path.resolve(resolved) === path.resolve(wrapperPath);
31
+ }
32
+ catch (err) {
33
+ debugLog(`isWrapperActive: which ${tool} failed: ${errorMessage(err)}`);
34
+ return false;
35
+ }
36
+ }
37
+ function commandVersion(cmd, args = ["--version"]) {
38
+ try {
39
+ return execFileSync(cmd, args, {
40
+ encoding: "utf8",
41
+ stdio: ["ignore", "pipe", "ignore"],
42
+ timeout: EXEC_TIMEOUT_QUICK_MS,
43
+ }).trim();
44
+ }
45
+ catch (err) {
46
+ debugLog(`doctor: commandVersion ${cmd} failed: ${errorMessage(err)}`);
47
+ return null;
48
+ }
49
+ }
50
+ function parseSemverTriple(raw) {
51
+ const match = raw.match(/(\d+)\.(\d+)\.(\d+)/);
52
+ if (!match)
53
+ return null;
54
+ return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)];
55
+ }
56
+ function versionAtLeast(raw, major, minor = 0) {
57
+ if (!raw)
58
+ return false;
59
+ const parsed = parseSemverTriple(raw);
60
+ if (!parsed)
61
+ return false;
62
+ const [m, n] = parsed;
63
+ if (m !== major)
64
+ return m > major;
65
+ return n >= minor;
66
+ }
67
+ function nearestWritableTarget(filePath) {
68
+ let probe = fs.existsSync(filePath) ? filePath : path.dirname(filePath);
69
+ while (!fs.existsSync(probe)) {
70
+ const parent = path.dirname(probe);
71
+ if (parent === probe)
72
+ return false;
73
+ probe = parent;
74
+ }
75
+ try {
76
+ fs.accessSync(probe, fs.constants.W_OK);
77
+ return true;
78
+ }
79
+ catch (err) {
80
+ debugLog(`doctor: writable check failed for ${filePath}: ${errorMessage(err)}`);
81
+ return false;
82
+ }
83
+ }
84
+ function gitRemoteStatus(phrenPath) {
85
+ try {
86
+ execFileSync("git", ["-C", phrenPath, "rev-parse", "--is-inside-work-tree"], {
87
+ stdio: ["ignore", "ignore", "ignore"],
88
+ timeout: EXEC_TIMEOUT_QUICK_MS,
89
+ });
90
+ }
91
+ catch {
92
+ return { ok: false, detail: "phren path is not a git repository" };
93
+ }
94
+ try {
95
+ const remote = execFileSync("git", ["-C", phrenPath, "remote", "get-url", "origin"], {
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "ignore"],
98
+ timeout: EXEC_TIMEOUT_QUICK_MS,
99
+ }).trim();
100
+ return remote
101
+ ? { ok: true, detail: `origin=${remote}` }
102
+ : { ok: true, detail: "no remote configured (local-only sync mode)" };
103
+ }
104
+ catch {
105
+ return { ok: true, detail: "no remote configured (local-only sync mode)" };
106
+ }
107
+ }
108
+ function pushSkillMirrorChecks(checks, scope, manifest, destDir) {
109
+ const parentDir = path.dirname(destDir);
110
+ checks.push({
111
+ name: `skills-manifest:${scope}`,
112
+ ok: fs.existsSync(path.join(parentDir, "skill-manifest.json")),
113
+ detail: fs.existsSync(path.join(parentDir, "skill-manifest.json"))
114
+ ? `generated: ${path.join(parentDir, "skill-manifest.json")}`
115
+ : `missing generated manifest at ${path.join(parentDir, "skill-manifest.json")}`,
116
+ });
117
+ checks.push({
118
+ name: `skills-commands:${scope}`,
119
+ ok: fs.existsSync(path.join(parentDir, "skill-commands.json")),
120
+ detail: fs.existsSync(path.join(parentDir, "skill-commands.json"))
121
+ ? `generated: ${path.join(parentDir, "skill-commands.json")}`
122
+ : `missing generated command registry at ${path.join(parentDir, "skill-commands.json")}`,
123
+ });
124
+ for (const skill of manifest.skills.filter((entry) => entry.visibleToAgents)) {
125
+ const dest = path.join(destDir, skill.format === "folder" ? skill.name : path.basename(skill.path));
126
+ let ok = false;
127
+ try {
128
+ ok = fs.existsSync(dest) && fs.realpathSync(dest) === fs.realpathSync(skill.root);
129
+ }
130
+ catch (err) {
131
+ debugLog(`doctor: skill mirror check failed for ${dest}: ${errorMessage(err)}`);
132
+ ok = false;
133
+ }
134
+ checks.push({
135
+ name: `skills-mirror:${scope}/${skill.name}`,
136
+ ok,
137
+ detail: ok ? "ok" : `missing/drifted link at ${dest}`,
138
+ });
139
+ }
140
+ for (const problem of manifest.problems) {
141
+ checks.push({
142
+ name: `skills-problem:${scope}:${problem.code}`,
143
+ ok: false,
144
+ detail: problem.message,
145
+ });
146
+ }
147
+ }
148
+ export async function runDoctor(phrenPath, fix = false, checkData = false) {
149
+ // Import runLink lazily to avoid circular dependency at module load time
150
+ const { runLink } = await import("./link.js");
151
+ const checks = [];
152
+ const machine = getMachineName();
153
+ const profile = lookupProfile(phrenPath, machine);
154
+ const gitVersion = commandVersion("git");
155
+ const nodeVersion = commandVersion("node");
156
+ checks.push({
157
+ name: "git-installed",
158
+ ok: Boolean(gitVersion),
159
+ detail: gitVersion || "git not found in PATH",
160
+ });
161
+ checks.push({
162
+ name: "node-version",
163
+ ok: versionAtLeast(nodeVersion, 20),
164
+ detail: nodeVersion || "node not found in PATH",
165
+ });
166
+ const gitRemote = gitRemoteStatus(phrenPath);
167
+ checks.push({
168
+ name: "git-remote",
169
+ ok: gitRemote.ok,
170
+ detail: gitRemote.detail,
171
+ });
172
+ checks.push({
173
+ name: "machine-registered",
174
+ ok: Boolean(profile),
175
+ detail: profile
176
+ ? `machine=${machine} profile=${profile}`
177
+ : `no profile mapping for machine=${machine} in machines.yaml`,
178
+ });
179
+ const profileFile = profile ? findProfileFile(phrenPath, profile) : null;
180
+ checks.push({
181
+ name: "profile-exists",
182
+ ok: Boolean(profileFile),
183
+ detail: profileFile ? `profile file found: ${profileFile}` : "profile file missing",
184
+ });
185
+ const projects = profileFile ? getProfileProjects(profileFile) : [];
186
+ checks.push({
187
+ name: "profile-projects",
188
+ ok: projects.length > 0,
189
+ detail: projects.length ? `${projects.length} projects in profile` : "no projects listed",
190
+ });
191
+ // Filesystem speed check
192
+ const fsBenchFile = path.join(phrenPath, ".fs-bench-tmp");
193
+ let fsMs = 0;
194
+ try {
195
+ const t0 = Date.now();
196
+ fs.writeFileSync(fsBenchFile, "phren-fs-check");
197
+ fs.readFileSync(fsBenchFile, "utf8");
198
+ fs.unlinkSync(fsBenchFile);
199
+ fsMs = Date.now() - t0;
200
+ }
201
+ catch (err) {
202
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
203
+ process.stderr.write(`[phren] doctor fsBenchmark: ${errorMessage(err)}\n`);
204
+ fsMs = -1;
205
+ try {
206
+ fs.unlinkSync(fsBenchFile);
207
+ }
208
+ catch (e2) {
209
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
210
+ process.stderr.write(`[phren] doctor fsBenchmarkCleanup: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
211
+ }
212
+ }
213
+ const fsSlow = fsMs > 500 || fsMs < 0;
214
+ checks.push({
215
+ name: "filesystem-speed",
216
+ ok: !fsSlow,
217
+ detail: fsMs < 0
218
+ ? "could not benchmark filesystem, check ~/.phren permissions"
219
+ : `write+read+delete in ${fsMs}ms${fsSlow ? " (slow, check if ~/.phren is on a network mount)" : ""}`,
220
+ });
221
+ const contextFile = homePath(".phren-context.md");
222
+ checks.push({
223
+ name: "context-file",
224
+ ok: fs.existsSync(contextFile),
225
+ detail: fs.existsSync(contextFile) ? contextFile : "missing ~/.phren-context.md",
226
+ });
227
+ const memoryFile = path.join(homeDir(), ".claude", "projects", claudeProjectKey(), "memory", "MEMORY.md");
228
+ checks.push({
229
+ name: "root-memory",
230
+ ok: fs.existsSync(memoryFile),
231
+ detail: fs.existsSync(memoryFile) ? memoryFile : "missing generated MEMORY.md",
232
+ });
233
+ const globalClaudeSrc = path.join(phrenPath, "global", "CLAUDE.md");
234
+ const globalClaudeDest = homePath(".claude", "CLAUDE.md");
235
+ let globalLinkOk = false;
236
+ try {
237
+ globalLinkOk = fs.existsSync(globalClaudeDest) && fs.realpathSync(globalClaudeDest) === fs.realpathSync(globalClaudeSrc);
238
+ }
239
+ catch (err) {
240
+ debugLog(`doctor: global CLAUDE.md symlink check failed: ${errorMessage(err)}`);
241
+ globalLinkOk = false;
242
+ }
243
+ checks.push({
244
+ name: "global-link",
245
+ ok: globalLinkOk,
246
+ detail: globalLinkOk ? "global CLAUDE.md symlink ok" : "global CLAUDE.md link drifted/missing",
247
+ });
248
+ pushSkillMirrorChecks(checks, "global", buildSkillManifest(phrenPath, profile || "", "global", homePath(".claude", "skills")), homePath(".claude", "skills"));
249
+ for (const project of projects) {
250
+ if (project === "global")
251
+ continue;
252
+ const config = readProjectConfig(phrenPath, project);
253
+ const ownership = getProjectOwnershipMode(phrenPath, project, config);
254
+ const target = findProjectDir(project);
255
+ if (ownership !== "phren-managed") {
256
+ checks.push({
257
+ name: `ownership:${project}`,
258
+ ok: true,
259
+ detail: `repo mirrors disabled (${ownership})`,
260
+ });
261
+ continue;
262
+ }
263
+ if (!target) {
264
+ checks.push({ name: `project-path:${project}`, ok: false, detail: "project directory not found on disk" });
265
+ continue;
266
+ }
267
+ for (const f of ["CLAUDE.md", "REFERENCE.md", "FINDINGS.md"]) {
268
+ const src = path.join(phrenPath, project, f);
269
+ if (!fs.existsSync(src))
270
+ continue;
271
+ const dest = path.join(target, f);
272
+ let ok = false;
273
+ try {
274
+ ok = fs.existsSync(dest) && fs.realpathSync(dest) === fs.realpathSync(src);
275
+ }
276
+ catch (err) {
277
+ debugLog(`doctor: symlink check failed for ${dest}: ${errorMessage(err)}`);
278
+ ok = false;
279
+ }
280
+ checks.push({
281
+ name: `symlink:${project}/${f}`,
282
+ ok,
283
+ detail: ok ? "ok" : `missing/drifted link at ${dest}`,
284
+ });
285
+ }
286
+ pushSkillMirrorChecks(checks, project, buildSkillManifest(phrenPath, profile || "", project, path.join(target, ".claude", "skills")), path.join(target, ".claude", "skills"));
287
+ }
288
+ const settingsPath = hookConfigPath("claude");
289
+ const configWritable = nearestWritableTarget(settingsPath);
290
+ checks.push({
291
+ name: "config-writable",
292
+ ok: configWritable,
293
+ detail: configWritable ? `writable: ${settingsPath}` : `not writable: ${settingsPath}`,
294
+ });
295
+ let hookOk = false;
296
+ let lifecycleOk = false;
297
+ try {
298
+ const cfg = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
299
+ const hooks = cfg?.hooks || {};
300
+ const promptHooks = JSON.stringify(hooks.UserPromptSubmit || []);
301
+ const stopHooks = JSON.stringify(hooks.Stop || []);
302
+ const startHooks = JSON.stringify(hooks.SessionStart || []);
303
+ hookOk = promptHooks.includes("hook-prompt");
304
+ const stopHookOk = stopHooks.includes("hook-stop");
305
+ const startHookOk = startHooks.includes("hook-session-start");
306
+ lifecycleOk = stopHookOk && startHookOk;
307
+ }
308
+ catch (err) {
309
+ debugLog(`doctor: failed to read Claude settings for hook check: ${errorMessage(err)}`);
310
+ hookOk = false;
311
+ lifecycleOk = false;
312
+ }
313
+ checks.push({
314
+ name: "claude-hooks",
315
+ ok: hookOk,
316
+ detail: hookOk ? "prompt hook configured" : "missing prompt hook in settings.json",
317
+ });
318
+ checks.push({
319
+ name: "lifecycle-hooks",
320
+ ok: lifecycleOk,
321
+ detail: lifecycleOk
322
+ ? "session-start + stop lifecycle hooks configured"
323
+ : "missing lifecycle hooks (expected hook-session-start and hook-stop)",
324
+ });
325
+ const runtimeHealthPath = runtimeHealthFile(phrenPath);
326
+ let runtime = null;
327
+ if (fs.existsSync(runtimeHealthPath)) {
328
+ try {
329
+ runtime = JSON.parse(fs.readFileSync(runtimeHealthPath, "utf8"));
330
+ }
331
+ catch (err) {
332
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
333
+ process.stderr.write(`[phren] doctor runtimeHealth: ${errorMessage(err)}\n`);
334
+ runtime = null;
335
+ }
336
+ }
337
+ checks.push({
338
+ name: "runtime-health-file",
339
+ ok: Boolean(runtime),
340
+ detail: runtime ? runtimeHealthPath : "missing or unreadable .runtime/runtime-health.json",
341
+ });
342
+ const lastAutoSave = runtime?.["lastAutoSave"];
343
+ const autoSaveObj = isRecord(lastAutoSave) ? lastAutoSave : null;
344
+ const autoSaveStatus = typeof autoSaveObj?.["status"] === "string" ? autoSaveObj["status"] : undefined;
345
+ const autoSaveAt = typeof autoSaveObj?.["at"] === "string" ? autoSaveObj["at"] : undefined;
346
+ checks.push({
347
+ name: "runtime-auto-save",
348
+ ok: autoSaveStatus === "saved-pushed" || autoSaveStatus === "saved-local" || autoSaveStatus === "clean",
349
+ detail: autoSaveStatus
350
+ ? `last auto-save: ${autoSaveStatus}${autoSaveAt ? ` @ ${autoSaveAt}` : ""}`
351
+ : "no auto-save runtime record yet",
352
+ });
353
+ checks.push({
354
+ name: "runtime-prompt",
355
+ ok: Boolean(runtime?.["lastPromptAt"]),
356
+ detail: runtime?.["lastPromptAt"] ? `last prompt hook run @ ${runtime["lastPromptAt"]}` : "no prompt runtime record yet",
357
+ });
358
+ try {
359
+ const db = await buildIndex(phrenPath, profile || undefined);
360
+ const healthRow = queryRows(db, "SELECT count(*) FROM docs", []);
361
+ const count = Number(healthRow?.[0]?.[0] ?? 0);
362
+ checks.push({
363
+ name: "fts-index",
364
+ ok: Number.isFinite(count) && count >= 0,
365
+ detail: `index query ok (docs=${count})`,
366
+ });
367
+ }
368
+ catch (err) {
369
+ checks.push({
370
+ name: "fts-index",
371
+ ok: false,
372
+ detail: `index build/query failed: ${errorMessage(err)}`,
373
+ });
374
+ }
375
+ const detected = detectInstalledTools();
376
+ if (detected.has("copilot")) {
377
+ const copilotHooks = hookConfigPath("copilot", phrenPath);
378
+ checks.push({
379
+ name: "copilot-hooks",
380
+ ok: fs.existsSync(copilotHooks),
381
+ detail: fs.existsSync(copilotHooks) ? "copilot hooks config present" : "missing copilot hooks config",
382
+ });
383
+ checks.push({
384
+ name: "copilot-config-writable",
385
+ ok: nearestWritableTarget(copilotHooks),
386
+ detail: nearestWritableTarget(copilotHooks) ? `writable: ${copilotHooks}` : `not writable: ${copilotHooks}`,
387
+ });
388
+ }
389
+ if (detected.has("cursor")) {
390
+ const cursorHooks = hookConfigPath("cursor", phrenPath);
391
+ checks.push({
392
+ name: "cursor-hooks",
393
+ ok: fs.existsSync(cursorHooks),
394
+ detail: fs.existsSync(cursorHooks) ? "cursor hooks config present" : "missing ~/.cursor/hooks.json",
395
+ });
396
+ checks.push({
397
+ name: "cursor-config-writable",
398
+ ok: nearestWritableTarget(cursorHooks),
399
+ detail: nearestWritableTarget(cursorHooks) ? `writable: ${cursorHooks}` : `not writable: ${cursorHooks}`,
400
+ });
401
+ }
402
+ if (detected.has("codex")) {
403
+ const codexHooks = hookConfigPath("codex", phrenPath);
404
+ checks.push({
405
+ name: "codex-hooks",
406
+ ok: fs.existsSync(codexHooks),
407
+ detail: fs.existsSync(codexHooks) ? "codex hooks config present" : "missing codex hooks config in phren root",
408
+ });
409
+ checks.push({
410
+ name: "codex-config-writable",
411
+ ok: nearestWritableTarget(codexHooks),
412
+ detail: nearestWritableTarget(codexHooks) ? `writable: ${codexHooks}` : `not writable: ${codexHooks}`,
413
+ });
414
+ }
415
+ for (const tool of ["copilot", "cursor", "codex"]) {
416
+ if (!detected.has(tool))
417
+ continue;
418
+ const active = isWrapperActive(tool);
419
+ checks.push({
420
+ name: `wrapper:${tool}`,
421
+ ok: active,
422
+ detail: active
423
+ ? `${tool} wrapper active via ~/.local/bin/${tool}`
424
+ : `${tool} wrapper missing or not first in PATH`,
425
+ });
426
+ }
427
+ if (fix) {
428
+ const repaired = repairPreexistingInstall(phrenPath);
429
+ const details = [];
430
+ if (repaired.removedLegacyProjects > 0)
431
+ details.push(`removed ${repaired.removedLegacyProjects} legacy sample profile entries`);
432
+ if (repaired.createdContextFile)
433
+ details.push("recreated ~/.phren-context.md");
434
+ if (repaired.createdRootMemory)
435
+ details.push("recreated generated MEMORY.md");
436
+ if (details.length === 0)
437
+ details.push("baseline repair complete");
438
+ checks.push({ name: "baseline-repair", ok: true, detail: details.join("; ") });
439
+ }
440
+ if (fix && profile && profileFile) {
441
+ await runLink(phrenPath, { machine, profile });
442
+ checks.push({ name: "self-heal", ok: true, detail: "relinked hooks, symlinks, context, memory pointers" });
443
+ }
444
+ else if (fix) {
445
+ checks.push({ name: "self-heal", ok: false, detail: "relink blocked: machine/profile not fully configured" });
446
+ }
447
+ else {
448
+ // Read-only mode: just check if hook configs exist, don't write anything
449
+ const detectedTools = detectInstalledTools();
450
+ const hookChecks = [];
451
+ const missing = [];
452
+ for (const tool of detectedTools) {
453
+ let configPath = "";
454
+ if (tool === "copilot" || tool === "cursor" || tool === "codex") {
455
+ configPath = hookConfigPath(tool, phrenPath);
456
+ }
457
+ if (configPath && fs.existsSync(configPath))
458
+ hookChecks.push(tool);
459
+ else if (configPath)
460
+ missing.push(tool);
461
+ }
462
+ checks.push({
463
+ name: "hooks",
464
+ ok: missing.length === 0,
465
+ detail: hookChecks.length
466
+ ? `hook configs present for: ${hookChecks.join(", ")}${missing.length ? `; missing: ${missing.join(", ")}` : ""}`
467
+ : detectedTools.size === 0
468
+ ? "no external tools detected"
469
+ : `missing hook configs for: ${missing.join(", ")}`,
470
+ });
471
+ }
472
+ if (checkData) {
473
+ const governanceChecks = [
474
+ { file: "retention-policy.json", schema: "retention-policy" },
475
+ { file: "workflow-policy.json", schema: "workflow-policy" },
476
+ { file: "index-policy.json", schema: "index-policy" },
477
+ ];
478
+ for (const item of governanceChecks) {
479
+ const filePath = path.join(phrenPath, ".governance", item.file);
480
+ const exists = fs.existsSync(filePath);
481
+ const valid = exists ? validateGovernanceJson(filePath, item.schema) : false;
482
+ checks.push({
483
+ name: `data:governance:${item.file}`,
484
+ ok: exists && valid,
485
+ detail: !exists ? "missing governance file" : valid ? "valid" : "invalid JSON/schema",
486
+ });
487
+ }
488
+ const runtimeChecks = [
489
+ { filePath: runtimeHealthFile(phrenPath), name: "data:runtime:runtime-health.json" },
490
+ ];
491
+ for (const item of runtimeChecks) {
492
+ const exists = fs.existsSync(item.filePath);
493
+ checks.push({
494
+ name: item.name,
495
+ ok: exists,
496
+ detail: exists ? "present" : "missing runtime file",
497
+ });
498
+ }
499
+ for (const projectDir of getProjectDirs(phrenPath, profile)) {
500
+ const projectName = path.basename(projectDir);
501
+ if (projectName === "global")
502
+ continue;
503
+ const taskPath = resolveTaskFilePath(phrenPath, projectName);
504
+ if (taskPath && fs.existsSync(taskPath)) {
505
+ const content = fs.readFileSync(taskPath, "utf8");
506
+ const issues = validateTaskFormat(content);
507
+ checks.push({
508
+ name: `data:tasks:${projectName}`,
509
+ ok: issues.length === 0,
510
+ detail: issues.length ? issues.join("; ") : "valid task file",
511
+ });
512
+ const repoPath = findProjectDir(projectName);
513
+ const hygiene = inspectTaskHygiene(phrenPath, projectName, repoPath);
514
+ checks.push({
515
+ name: `data:task-hygiene:${projectName}`,
516
+ ok: hygiene.ok,
517
+ detail: hygiene.detail,
518
+ });
519
+ }
520
+ const findingsPath = path.join(projectDir, "FINDINGS.md");
521
+ if (fs.existsSync(findingsPath)) {
522
+ const content = fs.readFileSync(findingsPath, "utf8");
523
+ const issues = validateFindingsFormat(content);
524
+ checks.push({
525
+ name: `data:findings:${projectName}`,
526
+ ok: issues.length === 0,
527
+ detail: issues.length ? issues.join("; ") : "valid",
528
+ });
529
+ }
530
+ }
531
+ // Detect conflict markers in project markdown files
532
+ for (const projectDir of getProjectDirs(phrenPath, profile)) {
533
+ const projectName = path.basename(projectDir);
534
+ if (projectName === "global")
535
+ continue;
536
+ for (const mdFile of ["FINDINGS.md", ...TASK_FILE_ALIASES, "MEMORY_QUEUE.md", "CLAUDE.md", "REFERENCE.md"]) {
537
+ const filePath = path.join(projectDir, mdFile);
538
+ if (!fs.existsSync(filePath))
539
+ continue;
540
+ const content = fs.readFileSync(filePath, "utf8");
541
+ const hasConflict = /^<{7} |^={7}$|^>{7} /m.test(content);
542
+ if (hasConflict) {
543
+ checks.push({
544
+ name: `data:conflict-markers:${projectName}/${mdFile}`,
545
+ ok: false,
546
+ detail: `${projectName}/${mdFile} contains git conflict markers`,
547
+ });
548
+ }
549
+ }
550
+ }
551
+ // Validate skill frontmatter in global/skills under phren data dir
552
+ const globalSkillsDir = path.join(phrenPath, "global", "skills");
553
+ const skillResults = validateSkillsDir(fs.existsSync(globalSkillsDir) ? globalSkillsDir : path.join(phrenPath, "global"));
554
+ const invalidSkills = skillResults.filter(r => !r.valid);
555
+ checks.push({
556
+ name: "data:skills-frontmatter",
557
+ ok: invalidSkills.length === 0,
558
+ detail: invalidSkills.length
559
+ ? `${invalidSkills.length} skill(s) with invalid frontmatter: ${invalidSkills.flatMap(r => r.errors).join("; ")}`
560
+ : `${skillResults.length} skill(s) validated`,
561
+ });
562
+ // Validate phren.SKILL.md manifest
563
+ const manifestPath = path.join(phrenPath, "phren.SKILL.md");
564
+ if (fs.existsSync(manifestPath)) {
565
+ const manifestResult = validateSkillFrontmatter(fs.readFileSync(manifestPath, "utf8"), manifestPath);
566
+ checks.push({
567
+ name: "data:skill-manifest",
568
+ ok: manifestResult.valid,
569
+ detail: manifestResult.valid ? "phren.SKILL.md frontmatter valid" : manifestResult.errors.join("; "),
570
+ });
571
+ }
572
+ // Verify file checksums
573
+ const checksumResults = verifyFileChecksums(phrenPath);
574
+ const mismatches = checksumResults.filter((r) => r.status === "mismatch");
575
+ const missingFiles = checksumResults.filter((r) => r.status === "missing");
576
+ if (checksumResults.length > 0) {
577
+ checks.push({
578
+ name: "data:file-checksums",
579
+ ok: mismatches.length === 0 && missingFiles.length === 0,
580
+ detail: mismatches.length || missingFiles.length
581
+ ? `${mismatches.length} mismatch(es), ${missingFiles.length} missing`
582
+ : `${checksumResults.length} file(s) verified`,
583
+ });
584
+ }
585
+ if (fix) {
586
+ updateFileChecksums(phrenPath, profile);
587
+ }
588
+ }
589
+ const ok = checks.every((c) => c.ok);
590
+ return { ok, machine, profile: profile || undefined, checks };
591
+ }