@phren/cli 0.0.10 → 0.0.11

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 (63) hide show
  1. package/README.md +2 -8
  2. package/mcp/dist/cli-actions.js +5 -5
  3. package/mcp/dist/cli-config.js +334 -127
  4. package/mcp/dist/cli-govern.js +35 -63
  5. package/mcp/dist/cli-graph.js +3 -2
  6. package/mcp/dist/cli-hooks-globs.js +2 -1
  7. package/mcp/dist/cli-hooks-output.js +3 -3
  8. package/mcp/dist/cli-hooks.js +39 -32
  9. package/mcp/dist/cli-namespaces.js +15 -5
  10. package/mcp/dist/cli-search.js +2 -2
  11. package/mcp/dist/content-archive.js +2 -2
  12. package/mcp/dist/content-dedup.js +9 -9
  13. package/mcp/dist/embedding.js +7 -7
  14. package/mcp/dist/entrypoint.js +129 -102
  15. package/mcp/dist/governance-locks.js +6 -5
  16. package/mcp/dist/governance-policy.js +155 -2
  17. package/mcp/dist/governance-scores.js +3 -3
  18. package/mcp/dist/hooks.js +39 -18
  19. package/mcp/dist/index.js +4 -4
  20. package/mcp/dist/init-config.js +3 -24
  21. package/mcp/dist/init-setup.js +5 -5
  22. package/mcp/dist/init.js +170 -23
  23. package/mcp/dist/link-checksums.js +3 -2
  24. package/mcp/dist/link-context.js +1 -1
  25. package/mcp/dist/link-doctor.js +3 -3
  26. package/mcp/dist/link-skills.js +98 -12
  27. package/mcp/dist/link.js +17 -27
  28. package/mcp/dist/machine-identity.js +1 -9
  29. package/mcp/dist/mcp-config.js +247 -42
  30. package/mcp/dist/mcp-data.js +9 -9
  31. package/mcp/dist/mcp-extract-facts.js +1 -1
  32. package/mcp/dist/mcp-extract.js +2 -2
  33. package/mcp/dist/mcp-finding.js +6 -6
  34. package/mcp/dist/mcp-graph.js +11 -11
  35. package/mcp/dist/mcp-ops.js +18 -18
  36. package/mcp/dist/mcp-search.js +8 -8
  37. package/mcp/dist/memory-ui-page.js +23 -0
  38. package/mcp/dist/memory-ui-scripts.js +210 -27
  39. package/mcp/dist/memory-ui-server.js +115 -3
  40. package/mcp/dist/phren-paths.js +7 -7
  41. package/mcp/dist/profile-store.js +2 -2
  42. package/mcp/dist/project-config.js +63 -16
  43. package/mcp/dist/session-utils.js +3 -2
  44. package/mcp/dist/shared-fragment-graph.js +22 -21
  45. package/mcp/dist/shared-index.js +144 -105
  46. package/mcp/dist/shared-retrieval.js +19 -13
  47. package/mcp/dist/shared-search-fallback.js +13 -13
  48. package/mcp/dist/shared-sqljs.js +3 -2
  49. package/mcp/dist/shared.js +3 -3
  50. package/mcp/dist/shell-input.js +1 -1
  51. package/mcp/dist/shell-state-store.js +1 -1
  52. package/mcp/dist/shell-view.js +3 -2
  53. package/mcp/dist/shell.js +1 -1
  54. package/mcp/dist/skill-files.js +4 -10
  55. package/mcp/dist/skill-registry.js +3 -0
  56. package/mcp/dist/status.js +41 -13
  57. package/mcp/dist/task-hygiene.js +1 -1
  58. package/mcp/dist/telemetry.js +5 -4
  59. package/mcp/dist/update.js +1 -1
  60. package/mcp/dist/utils.js +3 -3
  61. package/package.json +2 -2
  62. package/starter/global/skills/audit.md +106 -0
  63. package/mcp/dist/shared-paths.js +0 -1
@@ -144,7 +144,7 @@ export async function handlePruneMemories(args = []) {
144
144
  .filter((e) => e !== null);
145
145
  }
146
146
  catch (err) {
147
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
147
+ if ((process.env.PHREN_DEBUG))
148
148
  process.stderr.write(`[phren] cli-govern retrievalLog readParse: ${errorMessage(err)}\n`);
149
149
  }
150
150
  }
@@ -255,7 +255,7 @@ export async function handleGcMaintain(args = []) {
255
255
  const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
256
256
  let oldCommits = [];
257
257
  try {
258
- const raw = execSync(`git log --oneline --before="${sevenDaysAgo}" --format="%H %s"`, { cwd: phrenPath, encoding: "utf8" }).trim();
258
+ const raw = execSync(`git log --before="${sevenDaysAgo}" --format="%H %ci %s"`, { cwd: phrenPath, encoding: "utf8" }).trim();
259
259
  if (raw) {
260
260
  oldCommits = raw.split("\n").filter((l) => l.includes("auto-save:") || l.includes("[auto]"));
261
261
  }
@@ -271,23 +271,21 @@ export async function handleGcMaintain(args = []) {
271
271
  report.commitsSquashed = oldCommits.length;
272
272
  }
273
273
  else {
274
- // Group by ISO week (YYYY-Www) based on commit timestamp
274
+ // Group by ISO week based on commit timestamp (already in the log output)
275
275
  const commitsByWeek = new Map();
276
276
  for (const line of oldCommits) {
277
277
  const hash = line.split(" ")[0];
278
- try {
279
- const dateStr = execSync(`git log -1 --format="%ci" ${hash}`, { cwd: phrenPath, encoding: "utf8" }).trim();
280
- const date = new Date(dateStr);
281
- const weekStart = new Date(date);
282
- weekStart.setDate(date.getDate() - date.getDay()); // start of week (Sunday)
283
- const weekKey = weekStart.toISOString().slice(0, 10);
284
- if (!commitsByWeek.has(weekKey))
285
- commitsByWeek.set(weekKey, []);
286
- commitsByWeek.get(weekKey).push(hash);
287
- }
288
- catch {
289
- // Skip commits we can't resolve
290
- }
278
+ // Format: "HASH YYYY-MM-DD HH:MM:SS +ZZZZ subject..."
279
+ const dateMatch = line.match(/^[a-f0-9]+ (\d{4}-\d{2}-\d{2})/);
280
+ if (!dateMatch)
281
+ continue;
282
+ const date = new Date(dateMatch[1]);
283
+ const weekStart = new Date(date);
284
+ weekStart.setDate(date.getDate() - date.getDay());
285
+ const weekKey = weekStart.toISOString().slice(0, 10);
286
+ if (!commitsByWeek.has(weekKey))
287
+ commitsByWeek.set(weekKey, []);
288
+ commitsByWeek.get(weekKey).push(hash);
291
289
  }
292
290
  // For each week with multiple commits, soft-reset to oldest and amend into a summary
293
291
  for (const [weekKey, hashes] of commitsByWeek.entries()) {
@@ -320,64 +318,38 @@ export async function handleGcMaintain(args = []) {
320
318
  console.log("Commit squash: all old auto-save weeks have only one commit, nothing to squash.");
321
319
  }
322
320
  }
323
- // 3. Prune stale session markers from ~/.phren/.sessions/ older than 30 days
324
- const sessionsDir = path.join(phrenPath, ".sessions");
321
+ // 3–4. Prune stale files from .sessions/ and .runtime/
325
322
  const thirtyDaysAgo = Date.now() - 30 * 86400000;
326
- if (fs.existsSync(sessionsDir)) {
327
- const entries = fs.readdirSync(sessionsDir);
328
- for (const entry of entries) {
329
- const fullPath = path.join(sessionsDir, entry);
330
- try {
331
- const stat = fs.statSync(fullPath);
332
- if (stat.mtimeMs < thirtyDaysAgo) {
333
- if (dryRun) {
334
- console.log(`[dry-run] Would remove session marker: .sessions/${entry}`);
335
- }
336
- else {
337
- fs.unlinkSync(fullPath);
338
- }
339
- report.sessionsRemoved++;
340
- }
341
- }
342
- catch {
343
- // Skip unreadable entries
344
- }
345
- }
346
- }
347
- const sessionsVerb = dryRun ? "Would remove" : "Removed";
348
- console.log(`${sessionsVerb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
349
- // 4. Trim runtime logs from ~/.phren/.runtime/ older than 30 days
350
- const runtimeDir = path.join(phrenPath, ".runtime");
351
- const logExtensions = new Set([".log", ".jsonl", ".json"]);
352
- if (fs.existsSync(runtimeDir)) {
353
- const entries = fs.readdirSync(runtimeDir);
354
- for (const entry of entries) {
355
- const ext = path.extname(entry);
356
- if (!logExtensions.has(ext))
357
- continue;
358
- // Never trim the active audit log or telemetry config
359
- if (entry === "audit.log" || entry === "telemetry.json")
323
+ function pruneStaleFiles(dir, label, filter) {
324
+ let removed = 0;
325
+ if (!fs.existsSync(dir))
326
+ return removed;
327
+ for (const entry of fs.readdirSync(dir)) {
328
+ if (filter && !filter(entry))
360
329
  continue;
361
- const fullPath = path.join(runtimeDir, entry);
330
+ const fullPath = path.join(dir, entry);
362
331
  try {
363
- const stat = fs.statSync(fullPath);
364
- if (stat.mtimeMs < thirtyDaysAgo) {
332
+ if (fs.statSync(fullPath).mtimeMs < thirtyDaysAgo) {
365
333
  if (dryRun) {
366
- console.log(`[dry-run] Would remove runtime log: .runtime/${entry}`);
334
+ console.log(`[dry-run] Would remove: ${label}/${entry}`);
367
335
  }
368
336
  else {
369
337
  fs.unlinkSync(fullPath);
370
338
  }
371
- report.runtimeLogsRemoved++;
339
+ removed++;
372
340
  }
373
341
  }
374
- catch {
375
- // Skip unreadable entries
376
- }
342
+ catch { /* skip unreadable */ }
377
343
  }
344
+ return removed;
378
345
  }
379
- const logsVerb = dryRun ? "Would remove" : "Removed";
380
- console.log(`${logsVerb} ${report.runtimeLogsRemoved} stale runtime log(s) from .runtime/`);
346
+ const logExtensions = new Set([".log", ".jsonl", ".json"]);
347
+ const protectedFiles = new Set(["audit.log", "telemetry.json"]);
348
+ report.sessionsRemoved = pruneStaleFiles(path.join(phrenPath, ".sessions"), ".sessions");
349
+ report.runtimeLogsRemoved = pruneStaleFiles(path.join(phrenPath, ".runtime"), ".runtime", (entry) => logExtensions.has(path.extname(entry)) && !protectedFiles.has(entry));
350
+ const verb = dryRun ? "Would remove" : "Removed";
351
+ console.log(`${verb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
352
+ console.log(`${verb} ${report.runtimeLogsRemoved} stale runtime log(s) from .runtime/`);
381
353
  // 5. Summary
382
354
  if (!dryRun) {
383
355
  appendAuditLog(phrenPath, "maintain_gc", `gitGc=${report.gitGcRan} squashed=${report.commitsSquashed} sessions=${report.sessionsRemoved} logs=${report.runtimeLogsRemoved}`);
@@ -529,7 +501,7 @@ export async function handleBackgroundMaintenance(projectArg) {
529
501
  fs.unlinkSync(markers.lock);
530
502
  }
531
503
  catch (err) {
532
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
504
+ if ((process.env.PHREN_DEBUG))
533
505
  process.stderr.write(`[phren] cli-govern backgroundMaintenance unlockFinal: ${errorMessage(err)}\n`);
534
506
  }
535
507
  }
@@ -1,6 +1,7 @@
1
1
  import { getPhrenPath } from "./shared.js";
2
2
  import { buildIndex, queryRows } from "./shared-index.js";
3
3
  import { resolveRuntimeProfile } from "./runtime-profile.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  /**
5
6
  * CLI: phren graph [--project <name>] [--limit <n>]
6
7
  * Displays the fragment knowledge graph as a table.
@@ -115,7 +116,7 @@ export async function handleGraphLink(args) {
115
116
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, "mentions", sourceDoc]);
116
117
  }
117
118
  catch (err) {
118
- console.error(`Failed to link: ${err instanceof Error ? err.message : String(err)}`);
119
+ console.error(`Failed to link: ${errorMessage(err)}`);
119
120
  process.exit(1);
120
121
  }
121
122
  // Persist to manual-links.json
@@ -144,7 +145,7 @@ export async function handleGraphLink(args) {
144
145
  });
145
146
  }
146
147
  catch (err) {
147
- console.error(`Failed to persist manual link: ${err instanceof Error ? err.message : String(err)}`);
148
+ console.error(`Failed to persist manual link: ${errorMessage(err)}`);
148
149
  process.exit(1);
149
150
  }
150
151
  console.log(`Linked "${fragmentName}" to ${sourceDoc}.`);
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { capCache } from "./shared.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  // ── Glob matching and project frontmatter ────────────────────────────────────
5
6
  const projectGlobCache = new Map();
6
7
  export function clearProjectGlobCache() {
@@ -45,7 +46,7 @@ function parseProjectGlobs(phrenPathLocal, project) {
45
46
  }
46
47
  catch (err) {
47
48
  if (process.env.PHREN_DEBUG)
48
- process.stderr.write(`[phren] getProjectGlobs: ${err instanceof Error ? err.message : String(err)}\n`);
49
+ process.stderr.write(`[phren] getProjectGlobs: ${errorMessage(err)}\n`);
49
50
  }
50
51
  projectGlobCache.set(project, globs);
51
52
  capCache(projectGlobCache);
@@ -1,7 +1,7 @@
1
1
  import { recordInjection, recordRetrieval, } from "./shared-governance.js";
2
2
  import { getDocSourceKey, } from "./shared-index.js";
3
3
  import { logImpact, extractFindingIdsFromSnippet, } from "./finding-impact.js";
4
- import { isFeatureEnabled } from "./utils.js";
4
+ import { isFeatureEnabled, errorMessage } from "./utils.js";
5
5
  import { annotateStale } from "./cli-hooks-citations.js";
6
6
  import { approximateTokens, fileRelevanceBoost, branchMatchBoost } from "./shared-retrieval.js";
7
7
  // ── Progressive disclosure helpers ────────────────────────────────────────────
@@ -56,7 +56,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
56
56
  }
57
57
  catch (err) {
58
58
  if (process.env.PHREN_DEBUG)
59
- process.stderr.write(`[phren] injectContext recordRetrieval: ${err instanceof Error ? err.message : String(err)}\n`);
59
+ process.stderr.write(`[phren] injectContext recordRetrieval: ${errorMessage(err)}\n`);
60
60
  }
61
61
  }
62
62
  }
@@ -108,7 +108,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
108
108
  }
109
109
  catch (err) {
110
110
  if (process.env.PHREN_DEBUG)
111
- process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${err instanceof Error ? err.message : String(err)}\n`);
111
+ process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${errorMessage(err)}\n`);
112
112
  }
113
113
  parts.push(`[${getDocSourceKey(doc, phrenPathLocal)}] (${doc.type})`);
114
114
  parts.push(annotateStale(snippet));
@@ -5,7 +5,7 @@
5
5
  // cli-hooks-output.ts — hook output formatting
6
6
  // cli-hooks-globs.ts — project glob matching
7
7
  import { debugLog, sessionMarker, sessionsDir, getPhrenPath, } from "./shared.js";
8
- import { getRetentionPolicy, getWorkflowPolicy, } from "./shared-governance.js";
8
+ import { mergeConfig, } from "./shared-governance.js";
9
9
  import { buildIndex, detectProject, } from "./shared-index.js";
10
10
  import { isProjectHookEnabled } from "./project-config.js";
11
11
  import { checkConsolidationNeeded, } from "./shared-content.js";
@@ -126,7 +126,7 @@ export async function handleHookPrompt() {
126
126
  }
127
127
  catch (err) {
128
128
  if (process.env.PHREN_DEBUG)
129
- process.stderr.write(`[phren] hookPrompt stdinRead: ${err instanceof Error ? err.message : String(err)}\n`);
129
+ process.stderr.write(`[phren] hookPrompt stdinRead: ${errorMessage(err)}\n`);
130
130
  process.exit(0);
131
131
  }
132
132
  const input = parseHookInput(raw);
@@ -182,6 +182,7 @@ export async function handleHookPrompt() {
182
182
  appendAuditLog(getPhrenPath(), "hook_prompt", `status=project_disabled project=${detectedProject}`);
183
183
  process.exit(0);
184
184
  }
185
+ const resolvedConfig = mergeConfig(getPhrenPath(), detectedProject ?? undefined);
185
186
  const safeQuery = buildRobustFtsQuery(keywords, detectedProject, getPhrenPath());
186
187
  if (!safeQuery)
187
188
  process.exit(0);
@@ -193,14 +194,17 @@ export async function handleHookPrompt() {
193
194
  process.exit(0);
194
195
  autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
195
196
  const tTrust0 = Date.now();
196
- const policy = getRetentionPolicy(getPhrenPath());
197
+ const policy = resolvedConfig.retentionPolicy;
197
198
  const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
198
199
  const trustResult = applyTrustFilter(rows, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay, getPhrenPath());
199
200
  rows = trustResult.rows;
200
201
  stage.trustMs = Date.now() - tTrust0;
201
202
  if (!rows.length)
202
203
  process.exit(0);
203
- if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && getProactivityLevelForFindings(getPhrenPath()) !== "low" && sessionId && detectedProject && cwd) {
204
+ const findingsProactivity = resolvedConfig.proactivity.findings
205
+ ?? resolvedConfig.proactivity.base
206
+ ?? getProactivityLevelForFindings(getPhrenPath());
207
+ if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && findingsProactivity !== "low" && sessionId && detectedProject && cwd) {
204
208
  const marker = sessionMarker(getPhrenPath(), `extracted-${sessionId}-${detectedProject}`);
205
209
  if (!fs.existsSync(marker)) {
206
210
  try {
@@ -260,7 +264,9 @@ export async function handleHookPrompt() {
260
264
  debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
261
265
  }
262
266
  const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getPhrenPath(), sessionId);
263
- const taskLevel = getProactivityLevelForTask(getPhrenPath());
267
+ const taskLevel = resolvedConfig.proactivity.tasks
268
+ ?? resolvedConfig.proactivity.base
269
+ ?? getProactivityLevelForTask(getPhrenPath());
264
270
  const taskLifecycle = handleTaskPromptLifecycle({
265
271
  phrenPath: getPhrenPath(),
266
272
  prompt,
@@ -275,8 +281,7 @@ export async function handleHookPrompt() {
275
281
  }
276
282
  // Inject finding sensitivity agent instruction
277
283
  try {
278
- const workflowPolicy = getWorkflowPolicy(getPhrenPath());
279
- const sensitivity = workflowPolicy.findingSensitivity ?? "balanced";
284
+ const sensitivity = resolvedConfig.findingSensitivity ?? "balanced";
280
285
  const sensitivityConfig = FINDING_SENSITIVITY_CONFIG[sensitivity];
281
286
  if (sensitivityConfig) {
282
287
  parts.push("");
@@ -304,36 +309,38 @@ export async function handleHookPrompt() {
304
309
  const noticeFile = sessionId ? sessionMarker(getPhrenPath(), `noticed-${sessionId}`) : null;
305
310
  const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
306
311
  if (!alreadyNoticed) {
307
- // Clean up stale session markers (>24h old) from .sessions/ dir
308
- try {
309
- const cutoff = Date.now() - 86400000;
310
- const sessDir = sessionsDir(getPhrenPath());
311
- if (fs.existsSync(sessDir)) {
312
- for (const f of fs.readdirSync(sessDir)) {
313
- if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
312
+ // Defer stale session marker cleanup to after output — it's I/O-heavy and not needed for results
313
+ setImmediate(() => {
314
+ try {
315
+ const cutoff = Date.now() - 86400000;
316
+ const sessDir = sessionsDir(getPhrenPath());
317
+ if (fs.existsSync(sessDir)) {
318
+ for (const f of fs.readdirSync(sessDir)) {
319
+ if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
320
+ continue;
321
+ const fp = `${sessDir}/${f}`;
322
+ try {
323
+ if (fs.statSync(fp).mtimeMs < cutoff)
324
+ fs.unlinkSync(fp);
325
+ }
326
+ catch { /* ignore per-file errors */ }
327
+ }
328
+ }
329
+ // Also clean stale markers from the phren root
330
+ for (const f of fs.readdirSync(getPhrenPath())) {
331
+ if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
314
332
  continue;
315
- const fp = `${sessDir}/${f}`;
316
- if (fs.statSync(fp).mtimeMs < cutoff)
333
+ const fp = `${getPhrenPath()}/${f}`;
334
+ try {
317
335
  fs.unlinkSync(fp);
336
+ }
337
+ catch { /* ignore */ }
318
338
  }
319
339
  }
320
- // Also clean stale markers from the phren root
321
- for (const f of fs.readdirSync(getPhrenPath())) {
322
- if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
323
- continue;
324
- const fp = `${getPhrenPath()}/${f}`;
325
- try {
326
- fs.unlinkSync(fp);
327
- }
328
- catch (err) {
329
- if (process.env.PHREN_DEBUG)
330
- process.stderr.write(`[phren] hookPrompt staleNoticeUnlink: ${errorMessage(err)}\n`);
331
- }
340
+ catch (err) {
341
+ debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
332
342
  }
333
- }
334
- catch (err) {
335
- debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
336
- }
343
+ });
337
344
  const needed = checkConsolidationNeeded(getPhrenPath(), profile);
338
345
  if (needed.length > 0) {
339
346
  const notices = needed.map((n) => {
@@ -5,6 +5,7 @@ import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjec
5
5
  import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
7
7
  import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
8
+ import { detectSkillCollisions } from "./link-skills.js";
8
9
  import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
9
10
  import { findProjectDir } from "./project-locator.js";
10
11
  import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
@@ -76,7 +77,7 @@ function openInEditor(filePath) {
76
77
  execFileSync(editor, [filePath], { stdio: "inherit" });
77
78
  }
78
79
  catch (err) {
79
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
80
+ if ((process.env.PHREN_DEBUG))
80
81
  process.stderr.write(`[phren] openInEditor: ${errorMessage(err)}\n`);
81
82
  console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
82
83
  process.exit(1);
@@ -145,7 +146,7 @@ export function handleSkillsNamespace(args, profile) {
145
146
  console.log(`Linked skill ${fileName} into ${project}.`);
146
147
  }
147
148
  catch (err) {
148
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
149
+ if ((process.env.PHREN_DEBUG))
149
150
  process.stderr.write(`[phren] skill add symlinkFailed: ${errorMessage(err)}\n`);
150
151
  fs.copyFileSync(source, dest);
151
152
  console.log(`Copied skill ${fileName} into ${project}.`);
@@ -450,7 +451,7 @@ export function handleDetectSkills(args, profile) {
450
451
  continue;
451
452
  }
452
453
  catch (err) {
453
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
454
+ if ((process.env.PHREN_DEBUG))
454
455
  process.stderr.write(`[phren] skillList lstat: ${errorMessage(err)}\n`);
455
456
  }
456
457
  const name = entry.replace(/\.md$/, "");
@@ -548,6 +549,15 @@ function printSkillDoctor(scope, manifest, destDir) {
548
549
  problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
549
550
  }
550
551
  }
552
+ // Check for user-owned files blocking phren skill links
553
+ const phrenPath = getPhrenPath();
554
+ const srcDir = scope.toLowerCase() === "global"
555
+ ? path.join(phrenPath, "global", "skills")
556
+ : path.join(phrenPath, scope, "skills");
557
+ const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
558
+ for (const collision of collisions) {
559
+ problems.push(`Skill collision: ${collision.message}`);
560
+ }
551
561
  }
552
562
  if (!manifest.problems.length && !problems.length) {
553
563
  console.log("\nDoctor: no skill pipeline issues detected.");
@@ -853,7 +863,7 @@ function handleProjectsList(profile) {
853
863
  dirFiles = new Set(fs.readdirSync(projectDir));
854
864
  }
855
865
  catch (err) {
856
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
866
+ if ((process.env.PHREN_DEBUG))
857
867
  process.stderr.write(`[phren] projects list readdir: ${errorMessage(err)}\n`);
858
868
  dirFiles = new Set();
859
869
  }
@@ -896,7 +906,7 @@ async function handleProjectsRemove(name, profile) {
896
906
  countFiles(projectDir);
897
907
  }
898
908
  catch (err) {
899
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
909
+ if ((process.env.PHREN_DEBUG))
900
910
  process.stderr.write(`[phren] projects remove countFiles: ${errorMessage(err)}\n`);
901
911
  }
902
912
  const readline = await import("readline");
@@ -34,7 +34,7 @@ export function readSearchHistory(phrenPath) {
34
34
  .map((line) => JSON.parse(line));
35
35
  }
36
36
  catch (err) {
37
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
37
+ if ((process.env.PHREN_DEBUG))
38
38
  process.stderr.write(`[phren] readSearchHistory: ${errorMessage(err)}\n`);
39
39
  return [];
40
40
  }
@@ -253,7 +253,7 @@ export async function runSearch(opts, phrenPath, profile) {
253
253
  logSearchMiss(phrenPath, opts.query, opts.project);
254
254
  }
255
255
  catch (err) {
256
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
256
+ if ((process.env.PHREN_DEBUG))
257
257
  process.stderr.write(`[phren] search logSearchMiss: ${errorMessage(err)}\n`);
258
258
  }
259
259
  }
@@ -107,7 +107,7 @@ function isAlreadyArchived(referenceDir, bullet) {
107
107
  }
108
108
  }
109
109
  catch (err) {
110
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
110
+ if ((process.env.PHREN_DEBUG))
111
111
  process.stderr.write(`[phren] isDuplicateInReference: ${errorMessage(err)}\n`);
112
112
  }
113
113
  return false;
@@ -271,7 +271,7 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
271
271
  fs.unlinkSync(lockFile);
272
272
  }
273
273
  catch (err) {
274
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
274
+ if ((process.env.PHREN_DEBUG))
275
275
  process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
276
276
  }
277
277
  }
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
  import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "./shared.js";
5
- import { isFeatureEnabled, safeProjectPath } from "./utils.js";
5
+ import { isFeatureEnabled, safeProjectPath, errorMessage } from "./utils.js";
6
6
  import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "./phren-core.js";
7
7
  import { isInactiveFindingLine } from "./finding-lifecycle.js";
8
8
  // ── LLM provider abstraction ────────────────────────────────────────────────
@@ -50,8 +50,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
50
50
  }
51
51
  }
52
52
  catch (err) {
53
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
54
- process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
53
+ if ((process.env.PHREN_DEBUG))
54
+ process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
55
55
  }
56
56
  const result = await compute();
57
57
  // Persist result
@@ -61,8 +61,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
61
61
  persistCache(cachePath, cache);
62
62
  }
63
63
  catch (err) {
64
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
65
- process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
64
+ if ((process.env.PHREN_DEBUG))
65
+ process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
66
66
  }
67
67
  return result;
68
68
  }
@@ -562,8 +562,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
562
562
  return { name: e.name, mtime: fs.statSync(fp).mtimeMs, fp };
563
563
  }
564
564
  catch (err) {
565
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
566
- process.stderr.write(`[phren] crossProjectScan stat: ${err instanceof Error ? err.message : String(err)}\n`);
565
+ if ((process.env.PHREN_DEBUG))
566
+ process.stderr.write(`[phren] crossProjectScan stat: ${errorMessage(err)}\n`);
567
567
  return null;
568
568
  }
569
569
  })
@@ -577,8 +577,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
577
577
  }
578
578
  }
579
579
  catch (err) {
580
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
581
- process.stderr.write(`[phren] crossProjectScan: ${err instanceof Error ? err.message : String(err)}\n`);
580
+ if ((process.env.PHREN_DEBUG))
581
+ process.stderr.write(`[phren] crossProjectScan: ${errorMessage(err)}\n`);
582
582
  }
583
583
  const annotations = [];
584
584
  const deadline = Date.now() + CONFLICT_CHECK_TOTAL_TIMEOUT_MS;
@@ -79,7 +79,7 @@ async function openCacheDb(phrenPath) {
79
79
  db?.close();
80
80
  }
81
81
  catch (e2) {
82
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
82
+ if ((process.env.PHREN_DEBUG))
83
83
  process.stderr.write(`[phren] embedding openCacheDb dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
84
84
  }
85
85
  throw err;
@@ -126,13 +126,13 @@ function persistDb(phrenPath, db) {
126
126
  }
127
127
  }
128
128
  catch (err) {
129
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
130
- process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${err instanceof Error ? err.message : String(err)}\n`);
129
+ if ((process.env.PHREN_DEBUG))
130
+ process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${errorMessage(err)}\n`);
131
131
  try {
132
132
  onDisk?.close();
133
133
  }
134
134
  catch (e2) {
135
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
135
+ if ((process.env.PHREN_DEBUG))
136
136
  process.stderr.write(`[phren] embedding persistDb onDiskClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
137
137
  }
138
138
  onDisk = null;
@@ -149,7 +149,7 @@ function persistDb(phrenPath, db) {
149
149
  onDisk.close();
150
150
  }
151
151
  catch (e2) {
152
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
152
+ if ((process.env.PHREN_DEBUG))
153
153
  process.stderr.write(`[phren] embedding persistDb onDiskCloseFinally: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
154
154
  }
155
155
  }
@@ -269,7 +269,7 @@ export async function getCachedEmbedding(phrenPath, text, apiKey, model) {
269
269
  db?.close();
270
270
  }
271
271
  catch (e2) {
272
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
+ if ((process.env.PHREN_DEBUG))
273
273
  process.stderr.write(`[phren] embedding getCachedEmbedding dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
274
274
  }
275
275
  }
@@ -320,7 +320,7 @@ export async function getCachedEmbeddings(phrenPath, texts, apiKey, model) {
320
320
  db?.close();
321
321
  }
322
322
  catch (e2) {
323
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
323
+ if ((process.env.PHREN_DEBUG))
324
324
  process.stderr.write(`[phren] embedding getCachedEmbeddings dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
325
325
  }
326
326
  }