@phren/cli 0.0.10 → 0.0.12

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 (100) hide show
  1. package/README.md +11 -17
  2. package/mcp/dist/capabilities/cli.js +1 -1
  3. package/mcp/dist/capabilities/mcp.js +1 -1
  4. package/mcp/dist/capabilities/vscode.js +1 -1
  5. package/mcp/dist/capabilities/web-ui.js +1 -1
  6. package/mcp/dist/cli-actions.js +58 -71
  7. package/mcp/dist/cli-config.js +337 -131
  8. package/mcp/dist/cli-extract.js +3 -2
  9. package/mcp/dist/cli-govern.js +35 -63
  10. package/mcp/dist/cli-graph.js +19 -4
  11. package/mcp/dist/cli-hooks-globs.js +2 -1
  12. package/mcp/dist/cli-hooks-output.js +4 -4
  13. package/mcp/dist/cli-hooks-session.js +1 -1
  14. package/mcp/dist/cli-hooks.js +44 -35
  15. package/mcp/dist/cli-namespaces.js +15 -5
  16. package/mcp/dist/cli-search.js +2 -2
  17. package/mcp/dist/cli.js +1 -1
  18. package/mcp/dist/content-archive.js +23 -14
  19. package/mcp/dist/content-citation.js +13 -2
  20. package/mcp/dist/content-dedup.js +9 -9
  21. package/mcp/dist/content-learning.js +6 -4
  22. package/mcp/dist/content-metadata.js +10 -0
  23. package/mcp/dist/core-finding.js +1 -1
  24. package/mcp/dist/data-access.js +10 -31
  25. package/mcp/dist/data-tasks.js +5 -26
  26. package/mcp/dist/embedding.js +7 -8
  27. package/mcp/dist/entrypoint.js +133 -102
  28. package/mcp/dist/finding-impact.js +1 -32
  29. package/mcp/dist/finding-journal.js +1 -1
  30. package/mcp/dist/finding-lifecycle.js +2 -7
  31. package/mcp/dist/governance-locks.js +12 -5
  32. package/mcp/dist/governance-policy.js +156 -9
  33. package/mcp/dist/governance-scores.js +4 -10
  34. package/mcp/dist/hooks.js +62 -18
  35. package/mcp/dist/index.js +4 -4
  36. package/mcp/dist/init-config.js +4 -25
  37. package/mcp/dist/init-preferences.js +1 -1
  38. package/mcp/dist/init-setup.js +6 -55
  39. package/mcp/dist/init-shared.js +53 -1
  40. package/mcp/dist/init.js +191 -29
  41. package/mcp/dist/link-checksums.js +3 -2
  42. package/mcp/dist/link-context.js +2 -2
  43. package/mcp/dist/link-doctor.js +14 -57
  44. package/mcp/dist/link-skills.js +98 -12
  45. package/mcp/dist/link.js +16 -75
  46. package/mcp/dist/machine-identity.js +1 -9
  47. package/mcp/dist/mcp-config.js +247 -42
  48. package/mcp/dist/mcp-data.js +9 -9
  49. package/mcp/dist/mcp-extract-facts.js +12 -7
  50. package/mcp/dist/mcp-extract.js +2 -2
  51. package/mcp/dist/mcp-finding.js +16 -20
  52. package/mcp/dist/mcp-graph.js +12 -12
  53. package/mcp/dist/mcp-hooks.js +1 -1
  54. package/mcp/dist/mcp-ops.js +18 -18
  55. package/mcp/dist/mcp-search.js +11 -16
  56. package/mcp/dist/mcp-session.js +12 -2
  57. package/mcp/dist/memory-ui-assets.js +1 -36
  58. package/mcp/dist/memory-ui-graph.js +152 -50
  59. package/mcp/dist/memory-ui-page.js +30 -5
  60. package/mcp/dist/memory-ui-scripts.js +252 -63
  61. package/mcp/dist/memory-ui-server.js +115 -3
  62. package/mcp/dist/phren-core.js +2 -0
  63. package/mcp/dist/phren-paths.js +8 -9
  64. package/mcp/dist/proactivity.js +5 -5
  65. package/mcp/dist/profile-store.js +2 -2
  66. package/mcp/dist/project-config.js +64 -17
  67. package/mcp/dist/provider-adapters.js +1 -1
  68. package/mcp/dist/query-correlation.js +22 -19
  69. package/mcp/dist/session-checkpoints.js +14 -14
  70. package/mcp/dist/session-utils.js +3 -2
  71. package/mcp/dist/shared-data-utils.js +28 -0
  72. package/mcp/dist/shared-fragment-graph.js +22 -21
  73. package/mcp/dist/shared-governance.js +1 -1
  74. package/mcp/dist/shared-index.js +144 -105
  75. package/mcp/dist/shared-retrieval.js +21 -23
  76. package/mcp/dist/shared-search-fallback.js +15 -25
  77. package/mcp/dist/shared-sqljs.js +3 -2
  78. package/mcp/dist/shared.js +5 -6
  79. package/mcp/dist/shell-entry.js +1 -1
  80. package/mcp/dist/shell-input.js +63 -53
  81. package/mcp/dist/shell-palette.js +6 -1
  82. package/mcp/dist/shell-render.js +9 -5
  83. package/mcp/dist/shell-state-store.js +2 -5
  84. package/mcp/dist/shell-view.js +7 -6
  85. package/mcp/dist/shell.js +5 -55
  86. package/mcp/dist/skill-files.js +4 -10
  87. package/mcp/dist/skill-registry.js +3 -0
  88. package/mcp/dist/status.js +43 -21
  89. package/mcp/dist/task-hygiene.js +1 -1
  90. package/mcp/dist/telemetry.js +5 -4
  91. package/mcp/dist/update.js +1 -1
  92. package/mcp/dist/utils.js +4 -4
  93. package/package.json +2 -3
  94. package/skills/docs.md +11 -11
  95. package/starter/README.md +1 -1
  96. package/starter/global/CLAUDE.md +2 -2
  97. package/starter/global/skills/audit.md +106 -0
  98. package/mcp/dist/cli-hooks-retrieval.js +0 -2
  99. package/mcp/dist/impact-scoring.js +0 -22
  100. package/mcp/dist/shared-paths.js +0 -1
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import * as crypto from "crypto";
5
5
  import * as yaml from "js-yaml";
6
6
  import { bootstrapPhrenDotEnv } from "./phren-dotenv.js";
7
- import { PhrenError, isRecord } from "./phren-core.js";
7
+ import { PhrenError, isRecord, RESERVED_PROJECT_DIR_NAMES } from "./phren-core.js";
8
8
  import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
9
9
  bootstrapPhrenDotEnv();
10
10
  export const ROOT_MANIFEST_FILENAME = "phren.root.yaml";
@@ -74,7 +74,7 @@ export function readRootManifest(phrenPath) {
74
74
  return normalizeManifest(parsed);
75
75
  }
76
76
  catch (err) {
77
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
77
+ if ((process.env.PHREN_DEBUG))
78
78
  process.stderr.write(`[phren] readRootManifest: ${errorMessage(err)}\n`);
79
79
  return null;
80
80
  }
@@ -250,7 +250,7 @@ export function sessionMarker(phrenPath, name) {
250
250
  }
251
251
  // Debug logging is best-effort and only writes when a phren root already exists.
252
252
  export function debugLog(msg) {
253
- if (!(process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
253
+ if (!(process.env.PHREN_DEBUG))
254
254
  return;
255
255
  const phrenPath = findPhrenPath();
256
256
  if (!phrenPath)
@@ -269,7 +269,7 @@ export function appendIndexEvent(phrenPath, event) {
269
269
  fs.appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), ...event }) + "\n");
270
270
  }
271
271
  catch (err) {
272
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
+ if ((process.env.PHREN_DEBUG))
273
273
  process.stderr.write(`[phren] appendIndexEvent: ${errorMessage(err)}\n`);
274
274
  }
275
275
  }
@@ -280,7 +280,6 @@ export function resolveFindingsPath(projectDir) {
280
280
  return findingsPath;
281
281
  return undefined;
282
282
  }
283
- const RESERVED_PROJECT_DIR_NAMES = new Set(["profiles", "templates", "global"]);
284
283
  function isProjectDirEntry(entry) {
285
284
  return entry.isDirectory()
286
285
  && !entry.name.startsWith(".")
@@ -301,7 +300,7 @@ export function findProjectNameCaseInsensitive(phrenPath, name) {
301
300
  }
302
301
  }
303
302
  catch (err) {
304
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
303
+ if ((process.env.PHREN_DEBUG))
305
304
  process.stderr.write(`[phren] findProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
306
305
  }
307
306
  return null;
@@ -357,7 +356,7 @@ export function getProjectDirs(phrenPath, profile) {
357
356
  return [...new Set([...listed, ...sharedDirs])];
358
357
  }
359
358
  catch (err) {
360
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
359
+ if ((process.env.PHREN_DEBUG))
361
360
  process.stderr.write(`[phren] getProjectDirs yamlParse: ${errorMessage(err)}\n`);
362
361
  console.error(`${PhrenError.MALFORMED_YAML}: Malformed profile YAML: ${profilePath}`);
363
362
  return [];
@@ -369,7 +368,7 @@ export function getProjectDirs(phrenPath, profile) {
369
368
  .map((entry) => path.join(phrenPath, entry.name));
370
369
  }
371
370
  catch (err) {
372
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
371
+ if ((process.env.PHREN_DEBUG))
373
372
  process.stderr.write(`[phren] getProjectDirs: ${errorMessage(err)}\n`);
374
373
  return [];
375
374
  }
@@ -396,7 +395,7 @@ export function collectNativeMemoryFiles() {
396
395
  }
397
396
  }
398
397
  catch (err) {
399
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
398
+ if ((process.env.PHREN_DEBUG))
400
399
  process.stderr.write(`[phren] collectNativeMemoryFiles: ${errorMessage(err)}\n`);
401
400
  }
402
401
  return results;
@@ -93,21 +93,21 @@ function resolveProactivityLevel(raw, fallback) {
93
93
  }
94
94
  export function getProactivityLevel(explicitPhrenPath) {
95
95
  bootstrapPhrenDotEnv();
96
- return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityDefault(explicitPhrenPath));
96
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityDefault(explicitPhrenPath));
97
97
  }
98
98
  export function getProactivityLevelForFindings(explicitPhrenPath) {
99
99
  bootstrapPhrenDotEnv();
100
- const findingsPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_FINDINGS ?? (process.env.PHREN_PROACTIVITY_FINDINGS));
100
+ const findingsPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_FINDINGS);
101
101
  if (findingsPreference)
102
102
  return findingsPreference;
103
- return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath));
103
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath));
104
104
  }
105
105
  export function getProactivityLevelForTask(explicitPhrenPath) {
106
106
  bootstrapPhrenDotEnv();
107
- const taskPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_TASKS ?? (process.env.PHREN_PROACTIVITY_TASKS));
107
+ const taskPreference = parseProactivityLevel(process.env.PHREN_PROACTIVITY_TASKS);
108
108
  if (taskPreference)
109
109
  return taskPreference;
110
- return resolveProactivityLevel(process.env.PHREN_PROACTIVITY ?? (process.env.PHREN_PROACTIVITY), getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath));
110
+ return resolveProactivityLevel(process.env.PHREN_PROACTIVITY, getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath));
111
111
  }
112
112
  export function hasExplicitFindingSignal(...texts) {
113
113
  return texts.some((text) => {
@@ -73,7 +73,7 @@ export function listMachines(phrenPath) {
73
73
  return phrenOk(cleaned);
74
74
  }
75
75
  catch (err) {
76
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
76
+ if ((process.env.PHREN_DEBUG))
77
77
  process.stderr.write(`[phren] listMachines yaml parse: ${errorMessage(err)}\n`);
78
78
  return phrenErr(`Could not parse machines.yaml. Check the file for syntax errors or run 'phren doctor --fix'.`, PhrenError.MALFORMED_YAML);
79
79
  }
@@ -139,7 +139,7 @@ export function listProfiles(phrenPath) {
139
139
  profiles.push({ name, file: full, projects });
140
140
  }
141
141
  catch (err) {
142
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
142
+ if ((process.env.PHREN_DEBUG))
143
143
  process.stderr.write(`[phren] listProfiles yamlParse: ${errorMessage(err)}\n`);
144
144
  return phrenErr(`profiles/${file}`, PhrenError.MALFORMED_YAML);
145
145
  }
@@ -4,8 +4,8 @@ import * as path from "path";
4
4
  import * as yaml from "js-yaml";
5
5
  import { readInstallPreferences } from "./init-preferences.js";
6
6
  import { debugLog } from "./shared.js";
7
- import { errorMessage } from "./utils.js";
8
- import { withFileLock } from "./governance-locks.js";
7
+ import { errorMessage, safeProjectPath } from "./utils.js";
8
+ import { withFileLock } from "./shared-governance.js";
9
9
  export const PROJECT_OWNERSHIP_MODES = ["phren-managed", "detached", "repo-managed"];
10
10
  export const PROJECT_HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"];
11
11
  export function parseProjectOwnershipMode(raw) {
@@ -24,22 +24,58 @@ export function parseProjectOwnershipMode(raw) {
24
24
  export function projectConfigPath(phrenPath, project) {
25
25
  return path.join(phrenPath, project, "phren.project.yaml");
26
26
  }
27
+ function resolveProjectConfigPath(phrenPath, project) {
28
+ return safeProjectPath(phrenPath, project, "phren.project.yaml");
29
+ }
30
+ function writeProjectConfigFile(configPath, next) {
31
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
32
+ const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
33
+ fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
34
+ fs.renameSync(tmpPath, configPath);
35
+ _projectConfigCache.delete(configPath);
36
+ }
37
+ function normalizeProjectOverrides(raw) {
38
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
39
+ }
40
+ // ── mtime-based config cache ─────────────────────────────────────────────────
41
+ const _projectConfigCache = new Map();
42
+ export function clearProjectConfigCache() {
43
+ _projectConfigCache.clear();
44
+ }
27
45
  export function readProjectConfig(phrenPath, project) {
28
- const configPath = projectConfigPath(phrenPath, project);
29
- if (!fs.existsSync(configPath))
46
+ const configPath = resolveProjectConfigPath(phrenPath, project);
47
+ if (!configPath) {
48
+ debugLog(`readProjectConfig: rejected path for project "${project}"`);
49
+ return {};
50
+ }
51
+ let mtimeMs;
52
+ try {
53
+ mtimeMs = fs.statSync(configPath).mtimeMs;
54
+ }
55
+ catch {
56
+ // File doesn't exist or can't be stat'd
57
+ _projectConfigCache.delete(configPath);
30
58
  return {};
59
+ }
60
+ const cached = _projectConfigCache.get(configPath);
61
+ if (cached && cached.mtimeMs === mtimeMs) {
62
+ return cached.config;
63
+ }
31
64
  try {
32
65
  const parsed = yaml.load(fs.readFileSync(configPath, "utf8"), { schema: yaml.CORE_SCHEMA });
33
- return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
66
+ const config = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
67
+ _projectConfigCache.set(configPath, { mtimeMs, config });
68
+ return config;
34
69
  }
35
70
  catch (err) {
36
71
  debugLog(`readProjectConfig: failed to parse ${configPath}: ${errorMessage(err)}`);
72
+ _projectConfigCache.delete(configPath);
37
73
  return {};
38
74
  }
39
75
  }
40
76
  export function writeProjectConfig(phrenPath, project, patch) {
41
- const configPath = path.resolve(projectConfigPath(phrenPath, project));
42
- if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
77
+ const configPath = resolveProjectConfigPath(phrenPath, project);
78
+ if (!configPath) {
43
79
  throw new Error(`Project config path escapes phren store`);
44
80
  }
45
81
  return withFileLock(configPath, () => {
@@ -48,10 +84,24 @@ export function writeProjectConfig(phrenPath, project, patch) {
48
84
  ...current,
49
85
  ...patch,
50
86
  };
51
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
52
- const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
53
- fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
54
- fs.renameSync(tmpPath, configPath);
87
+ writeProjectConfigFile(configPath, next);
88
+ return next;
89
+ });
90
+ }
91
+ export function updateProjectConfigOverrides(phrenPath, project, updater) {
92
+ const configPath = resolveProjectConfigPath(phrenPath, project);
93
+ if (!configPath) {
94
+ throw new Error(`Project config path escapes phren store`);
95
+ }
96
+ return withFileLock(configPath, () => {
97
+ const current = readProjectConfig(phrenPath, project);
98
+ const currentConfig = normalizeProjectOverrides(current.config);
99
+ const nextOverrides = normalizeProjectOverrides(updater(currentConfig));
100
+ const next = {
101
+ ...current,
102
+ config: nextOverrides,
103
+ };
104
+ writeProjectConfigFile(configPath, next);
55
105
  return next;
56
106
  });
57
107
  }
@@ -82,8 +132,8 @@ export function isProjectHookEnabled(phrenPath, project, event, config) {
82
132
  }
83
133
  export function writeProjectHookConfig(phrenPath, project, patch) {
84
134
  // Move read+merge inside the lock so concurrent writers cannot clobber each other.
85
- const configPath = path.resolve(projectConfigPath(phrenPath, project));
86
- if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
135
+ const configPath = resolveProjectConfigPath(phrenPath, project);
136
+ if (!configPath) {
87
137
  throw new Error(`Project config path escapes phren store`);
88
138
  }
89
139
  return withFileLock(configPath, () => {
@@ -95,10 +145,7 @@ export function writeProjectHookConfig(phrenPath, project, patch) {
95
145
  ...patch,
96
146
  },
97
147
  };
98
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
99
- const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
100
- fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
101
- fs.renameSync(tmpPath, configPath);
148
+ writeProjectConfigFile(configPath, next);
102
149
  return next;
103
150
  });
104
151
  }
@@ -18,7 +18,7 @@ function homePathForEnv(env, ...parts) {
18
18
  return joinPortable(homeDir(env), ...parts);
19
19
  }
20
20
  function defaultPhrenPath(env = process.env) {
21
- return env.PHREN_PATH || env.PHREN_PATH || homePathForEnv(env, ".phren");
21
+ return env.PHREN_PATH || homePathForEnv(env, ".phren");
22
22
  }
23
23
  function normalizeWindowsPathToWsl(input) {
24
24
  if (!input)
@@ -7,6 +7,7 @@
7
7
  import * as fs from "fs";
8
8
  import { runtimeFile, debugLog } from "./shared.js";
9
9
  import { isFeatureEnabled, errorMessage } from "./utils.js";
10
+ import { withFileLock } from "./shared-governance.js";
10
11
  const CORRELATION_FILENAME = "query-correlations.jsonl";
11
12
  const RECENT_WINDOW = 500;
12
13
  const MIN_TOKEN_OVERLAP = 2;
@@ -56,28 +57,30 @@ export function markCorrelationsHelpful(phrenPath, sessionId, docKey) {
56
57
  const correlationFile = runtimeFile(phrenPath, CORRELATION_FILENAME);
57
58
  if (!fs.existsSync(correlationFile))
58
59
  return;
59
- const raw = fs.readFileSync(correlationFile, "utf8");
60
- const lines = raw.split("\n").filter(Boolean);
61
- let modified = false;
62
- const updated = lines.map((line) => {
63
- try {
64
- const entry = JSON.parse(line);
65
- if (entry.sessionId === sessionId &&
66
- `${entry.project}/${entry.filename}` === docKey &&
67
- !entry.helpful) {
68
- entry.helpful = true;
69
- modified = true;
70
- return JSON.stringify(entry);
60
+ withFileLock(correlationFile, () => {
61
+ const raw = fs.readFileSync(correlationFile, "utf8");
62
+ const lines = raw.split("\n").filter(Boolean);
63
+ let modified = false;
64
+ const updated = lines.map((line) => {
65
+ try {
66
+ const entry = JSON.parse(line);
67
+ if (entry.sessionId === sessionId &&
68
+ `${entry.project}/${entry.filename}` === docKey &&
69
+ !entry.helpful) {
70
+ entry.helpful = true;
71
+ modified = true;
72
+ return JSON.stringify(entry);
73
+ }
71
74
  }
75
+ catch {
76
+ // keep original line
77
+ }
78
+ return line;
79
+ });
80
+ if (modified) {
81
+ fs.writeFileSync(correlationFile, updated.join("\n") + "\n");
72
82
  }
73
- catch {
74
- // keep original line
75
- }
76
- return line;
77
83
  });
78
- if (modified) {
79
- fs.writeFileSync(correlationFile, updated.join("\n") + "\n");
80
- }
81
84
  }
82
85
  catch (err) {
83
86
  debugLog(`query-correlation mark-helpful failed: ${errorMessage(err)}`);
@@ -110,21 +110,21 @@ export function clearTaskCheckpoint(phrenPath, args) {
110
110
  debugLog(`checkpoint clear ${filePath}: ${errorMessage(err)}`);
111
111
  }
112
112
  }
113
- const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
114
- for (const checkpoint of allProjectCheckpoints) {
115
- const idMatch = ids.size > 0 && ids.has(checkpoint.taskId);
116
- const lineMatch = args.taskLine && checkpoint.taskLine === args.taskLine;
117
- if (!idMatch && !lineMatch)
118
- continue;
119
- const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
120
- try {
121
- if (fs.existsSync(filePath)) {
122
- fs.unlinkSync(filePath);
123
- removed++;
113
+ if (args.taskLine) {
114
+ const allProjectCheckpoints = listTaskCheckpoints(phrenPath, args.project);
115
+ for (const checkpoint of allProjectCheckpoints) {
116
+ if (checkpoint.taskLine !== args.taskLine)
117
+ continue;
118
+ const filePath = checkpointPath(phrenPath, checkpoint.project, checkpoint.taskId);
119
+ try {
120
+ if (fs.existsSync(filePath)) {
121
+ fs.unlinkSync(filePath);
122
+ removed++;
123
+ }
124
+ }
125
+ catch (err) {
126
+ debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
124
127
  }
125
- }
126
- catch (err) {
127
- debugLog(`checkpoint clear scan ${filePath}: ${errorMessage(err)}`);
128
128
  }
129
129
  }
130
130
  return removed;
@@ -1,5 +1,6 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
+ import { errorMessage } from "./utils.js";
3
4
  /**
4
5
  * Write JSON to a file atomically using temp-file + rename.
5
6
  * Ensures the parent directory exists before writing.
@@ -15,8 +16,8 @@ export function atomicWriteJson(filePath, data) {
15
16
  * Centralises the repeated `if (PHREN_DEBUG) stderr.write(...)` pattern.
16
17
  */
17
18
  export function debugError(scope, err) {
18
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
19
- process.stderr.write(`[phren] ${scope}: ${err instanceof Error ? err.message : String(err)}\n`);
19
+ if ((process.env.PHREN_DEBUG)) {
20
+ process.stderr.write(`[phren] ${scope}: ${errorMessage(err)}\n`);
20
21
  }
21
22
  }
22
23
  /**
@@ -0,0 +1,28 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { phrenErr, PhrenError, phrenOk, } from "./shared.js";
4
+ import { withFileLock as withFileLockRaw } from "./shared-governance.js";
5
+ import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
6
+ export function withSafeLock(filePath, fn) {
7
+ try {
8
+ return withFileLockRaw(filePath, fn);
9
+ }
10
+ catch (err) {
11
+ const msg = errorMessage(err);
12
+ if (msg.includes("could not acquire lock")) {
13
+ return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
14
+ }
15
+ throw err;
16
+ }
17
+ }
18
+ export function ensureProject(phrenPath, project) {
19
+ if (!isValidProjectName(project))
20
+ return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
21
+ const dir = safeProjectPath(phrenPath, project);
22
+ if (!dir)
23
+ return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
24
+ if (!fs.existsSync(dir)) {
25
+ return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
26
+ }
27
+ return phrenOk(dir);
28
+ }
@@ -2,6 +2,7 @@ import { decodeStringRow } from "./shared-index.js";
2
2
  import * as fs from "fs";
3
3
  import { runtimeFile } from "./shared.js";
4
4
  import { UNIVERSAL_TECH_TERMS_RE } from "./phren-core.js";
5
+ import { errorMessage } from "./utils.js";
5
6
  export function escapeRegex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
6
7
  /** Escape SQL LIKE wildcard characters so user input is treated literally. */
7
8
  export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
@@ -21,7 +22,7 @@ export function escapeLike(s) { return s.replace(/[%_\\]/g, '\\$&'); }
21
22
  * found" -> suggest adding it).
22
23
  */
23
24
  export function logFragmentMiss(phrenPath, name, context, project) {
24
- if (!process.env.PHREN_DEBUG && !(process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
25
+ if (!process.env.PHREN_DEBUG)
25
26
  return;
26
27
  if (!name || name.length <= 2)
27
28
  return;
@@ -115,8 +116,8 @@ function getOrCreateFragment(db, name, type) {
115
116
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [name, type, new Date().toISOString().slice(0, 10)]);
116
117
  }
117
118
  catch (err) {
118
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
119
- process.stderr.write(`[phren] fragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
119
+ if (process.env.PHREN_DEBUG)
120
+ process.stderr.write(`[phren] fragmentInsert: ${errorMessage(err)}\n`);
120
121
  }
121
122
  const result = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [name, type]);
122
123
  if (result?.length && result[0]?.values?.length) {
@@ -138,8 +139,8 @@ export function ensureGlobalEntitiesTable(db) {
138
139
  )`);
139
140
  }
140
141
  catch (err) {
141
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
142
- process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${err instanceof Error ? err.message : String(err)}\n`);
142
+ if (process.env.PHREN_DEBUG)
143
+ process.stderr.write(`[phren] ensureGlobalEntitiesTable: ${errorMessage(err)}\n`);
143
144
  }
144
145
  }
145
146
  /**
@@ -185,8 +186,8 @@ export function beginUserFragmentBuildCache(phrenPath, projects) {
185
186
  _buildUserFragmentCache.set(cacheKey, loaded.fragments);
186
187
  }
187
188
  catch (err) {
188
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
189
- process.stderr.write(`[phren] beginUserFragmentBuildCache: ${err instanceof Error ? err.message : String(err)}\n`);
189
+ if (process.env.PHREN_DEBUG)
190
+ process.stderr.write(`[phren] beginUserFragmentBuildCache: ${errorMessage(err)}\n`);
190
191
  _buildUserFragmentCache.set(cacheKey, []);
191
192
  }
192
193
  }
@@ -224,8 +225,8 @@ function parseUserDefinedFragments(phrenPath, project) {
224
225
  }
225
226
  }
226
227
  catch (err) {
227
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
228
- process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${err instanceof Error ? err.message : String(err)}\n`);
228
+ if (process.env.PHREN_DEBUG)
229
+ process.stderr.write(`[phren] parseUserDefinedFragments statCheck: ${errorMessage(err)}\n`);
229
230
  }
230
231
  }
231
232
  const loaded = readUserDefinedFragmentsFromDisk(claudeMdPath);
@@ -235,8 +236,8 @@ function parseUserDefinedFragments(phrenPath, project) {
235
236
  return loaded.fragments;
236
237
  }
237
238
  catch (err) {
238
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
239
- process.stderr.write(`[phren] parseUserDefinedFragments: ${err instanceof Error ? err.message : String(err)}\n`);
239
+ if (process.env.PHREN_DEBUG)
240
+ process.stderr.write(`[phren] parseUserDefinedFragments: ${errorMessage(err)}\n`);
240
241
  return [];
241
242
  }
242
243
  }
@@ -352,8 +353,8 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
352
353
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [docFragmentId, fragmentId, "mentions", sourceDoc]);
353
354
  }
354
355
  catch (err) {
355
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
356
- process.stderr.write(`[phren] fragmentLinksInsert: ${err instanceof Error ? err.message : String(err)}\n`);
356
+ if (process.env.PHREN_DEBUG)
357
+ process.stderr.write(`[phren] fragmentLinksInsert: ${errorMessage(err)}\n`);
357
358
  }
358
359
  // Write to global_entities for cross-project queries
359
360
  if (project) {
@@ -361,8 +362,8 @@ export function extractAndLinkFragments(db, content, sourceDoc, phrenPath) {
361
362
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [name, project, sourceDoc]);
362
363
  }
363
364
  catch (err) {
364
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
365
- process.stderr.write(`[phren] globalFragmentsInsert: ${err instanceof Error ? err.message : String(err)}\n`);
365
+ if (process.env.PHREN_DEBUG)
366
+ process.stderr.write(`[phren] globalFragmentsInsert: ${errorMessage(err)}\n`);
366
367
  }
367
368
  }
368
369
  }
@@ -390,8 +391,8 @@ export function queryFragmentLinks(db, name) {
390
391
  }
391
392
  }
392
393
  catch (err) {
393
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
394
- process.stderr.write(`[phren] queryFragmentLinks: ${err instanceof Error ? err.message : String(err)}\n`);
394
+ if (process.env.PHREN_DEBUG)
395
+ process.stderr.write(`[phren] queryFragmentLinks: ${errorMessage(err)}\n`);
395
396
  }
396
397
  return { related };
397
398
  }
@@ -426,8 +427,8 @@ export function queryCrossProjectFragments(db, fragmentName, excludeProject) {
426
427
  }
427
428
  }
428
429
  catch (err) {
429
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
430
- process.stderr.write(`[phren] queryCrossProjectFragments: ${err instanceof Error ? err.message : String(err)}\n`);
430
+ if (process.env.PHREN_DEBUG)
431
+ process.stderr.write(`[phren] queryCrossProjectFragments: ${errorMessage(err)}\n`);
431
432
  }
432
433
  return results;
433
434
  }
@@ -447,8 +448,8 @@ export function getFragmentBoostDocs(db, query) {
447
448
  return boostDocs;
448
449
  }
449
450
  catch (err) {
450
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
451
- process.stderr.write(`[phren] getFragmentBoostDocs: ${err instanceof Error ? err.message : String(err)}\n`);
451
+ if (process.env.PHREN_DEBUG)
452
+ process.stderr.write(`[phren] getFragmentBoostDocs: ${errorMessage(err)}\n`);
452
453
  return new Set();
453
454
  }
454
455
  }
@@ -1,4 +1,4 @@
1
1
  export * from './governance-policy.js';
2
2
  export * from './governance-scores.js';
3
3
  export * from './governance-audit.js';
4
- export { withFileLock } from './governance-locks.js';
4
+ export { withFileLock, isFiniteNumber, hasValidSchemaVersion } from './governance-locks.js';