@phren/cli 0.0.9 → 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 (67) 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 +140 -3
  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 +41 -34
  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-citation.js +12 -22
  13. package/mcp/dist/content-dedup.js +9 -9
  14. package/mcp/dist/data-access.js +1 -1
  15. package/mcp/dist/data-tasks.js +23 -0
  16. package/mcp/dist/embedding.js +7 -7
  17. package/mcp/dist/entrypoint.js +129 -102
  18. package/mcp/dist/governance-locks.js +6 -5
  19. package/mcp/dist/governance-policy.js +155 -2
  20. package/mcp/dist/governance-scores.js +3 -3
  21. package/mcp/dist/hooks.js +39 -18
  22. package/mcp/dist/index.js +4 -4
  23. package/mcp/dist/init-config.js +3 -24
  24. package/mcp/dist/init-setup.js +5 -5
  25. package/mcp/dist/init.js +170 -23
  26. package/mcp/dist/link-checksums.js +3 -2
  27. package/mcp/dist/link-context.js +1 -1
  28. package/mcp/dist/link-doctor.js +3 -3
  29. package/mcp/dist/link-skills.js +98 -12
  30. package/mcp/dist/link.js +17 -27
  31. package/mcp/dist/machine-identity.js +1 -9
  32. package/mcp/dist/mcp-config.js +247 -42
  33. package/mcp/dist/mcp-data.js +9 -9
  34. package/mcp/dist/mcp-extract-facts.js +1 -1
  35. package/mcp/dist/mcp-extract.js +2 -2
  36. package/mcp/dist/mcp-finding.js +6 -6
  37. package/mcp/dist/mcp-graph.js +11 -11
  38. package/mcp/dist/mcp-ops.js +18 -18
  39. package/mcp/dist/mcp-search.js +8 -8
  40. package/mcp/dist/mcp-tasks.js +21 -1
  41. package/mcp/dist/memory-ui-page.js +23 -0
  42. package/mcp/dist/memory-ui-scripts.js +210 -27
  43. package/mcp/dist/memory-ui-server.js +115 -3
  44. package/mcp/dist/phren-paths.js +7 -7
  45. package/mcp/dist/profile-store.js +2 -2
  46. package/mcp/dist/project-config.js +63 -16
  47. package/mcp/dist/session-utils.js +3 -2
  48. package/mcp/dist/shared-fragment-graph.js +22 -21
  49. package/mcp/dist/shared-index.js +144 -105
  50. package/mcp/dist/shared-retrieval.js +22 -56
  51. package/mcp/dist/shared-search-fallback.js +13 -13
  52. package/mcp/dist/shared-sqljs.js +3 -2
  53. package/mcp/dist/shared.js +3 -3
  54. package/mcp/dist/shell-input.js +1 -1
  55. package/mcp/dist/shell-state-store.js +1 -1
  56. package/mcp/dist/shell-view.js +3 -2
  57. package/mcp/dist/shell.js +1 -1
  58. package/mcp/dist/skill-files.js +4 -10
  59. package/mcp/dist/skill-registry.js +3 -0
  60. package/mcp/dist/status.js +41 -13
  61. package/mcp/dist/task-hygiene.js +1 -1
  62. package/mcp/dist/telemetry.js +5 -4
  63. package/mcp/dist/update.js +1 -1
  64. package/mcp/dist/utils.js +3 -3
  65. package/package.json +2 -2
  66. package/starter/global/skills/audit.md +106 -0
  67. package/mcp/dist/shared-paths.js +0 -1
@@ -3,8 +3,8 @@ import { getQualityMultiplier, entryScoreKey, } from "./shared-governance.js";
3
3
  import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getEntityBoostDocs, decodeFiniteNumber, rowToDocWithRowid, } from "./shared-index.js";
4
4
  import { filterTrustedFindingsDetailed, } from "./shared-content.js";
5
5
  import { parseCitationComment } from "./content-citation.js";
6
- import { getHighImpactFindings, getImpactSurfaceCounts } from "./finding-impact.js";
7
- import { buildFtsQueryVariants, buildRelaxedFtsQuery, isFeatureEnabled, STOP_WORDS } from "./utils.js";
6
+ import { getHighImpactFindings } from "./finding-impact.js";
7
+ import { buildFtsQueryVariants, buildRelaxedFtsQuery, isFeatureEnabled, STOP_WORDS, errorMessage } from "./utils.js";
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
10
  import { getProjectGlobBoost } from "./cli-hooks-globs.js";
@@ -34,23 +34,27 @@ const TASK_RESCUE_SCORE_MARGIN = 0.6;
34
34
  /** Fraction of bullets that must be low-value before applying the low-value penalty. */
35
35
  const LOW_VALUE_BULLET_FRACTION = 0.5;
36
36
  // ── Intent and scoring helpers ───────────────────────────────────────────────
37
+ const INTENT_SKILL_CMD_RE = /(?:^|\s)\/(?!(?:home|usr|var|tmp|etc|opt|api|mnt)\b)[a-z][\w-]*\b/;
38
+ const INTENT_SKILL_KW_RE = /\bskill\b/;
39
+ const INTENT_DEBUG_RE = /(bug|error|fix|broken|regression|fail|stack trace)/;
40
+ const INTENT_REVIEW_RE = /(review|audit|pr|pull request|nit|refactor)/;
41
+ const INTENT_BUILD_RE = /(build|deploy|release|ci|workflow|pipeline|test)/;
42
+ const INTENT_DOCS_RE = /\b(doc|docs|readme|explain|guide|instructions?)\b/;
37
43
  export function detectTaskIntent(prompt) {
38
44
  const p = prompt.toLowerCase();
39
- if (/(^|\s)\/[a-z][a-z0-9_-]{1,63}(?=$|\s|[.,:;!?])/.test(p) || /\b(skill|swarm|lineup|slash command)\b/.test(p))
45
+ if (INTENT_SKILL_CMD_RE.test(p) || INTENT_SKILL_KW_RE.test(p))
40
46
  return "skill";
41
- if (/(bug|error|fix|broken|regression|fail|stack trace)/.test(p))
47
+ if (INTENT_DEBUG_RE.test(p))
42
48
  return "debug";
43
- if (/(review|audit|pr|pull request|nit|refactor)/.test(p))
49
+ if (INTENT_REVIEW_RE.test(p))
44
50
  return "review";
45
- if (/(build|deploy|release|ci|workflow|pipeline|test)/.test(p))
51
+ if (INTENT_BUILD_RE.test(p))
46
52
  return "build";
47
- if (/\b(doc|docs|readme|explain|guide|instructions?)\b/.test(p))
53
+ if (INTENT_DOCS_RE.test(p))
48
54
  return "docs";
49
55
  return "general";
50
56
  }
51
57
  function intentBoost(intent, docType) {
52
- if (intent === "skill" && docType === "skill")
53
- return 4;
54
58
  if (intent === "debug" && (docType === "findings" || docType === "reference"))
55
59
  return 3;
56
60
  if (intent === "review" && (docType === "canonical" || docType === "changelog"))
@@ -347,23 +351,10 @@ export function searchDocuments(db, safeQuery, prompt, keywords, detectedProject
347
351
  if (ftsDocs.length === 0 && relaxedQuery && relaxedQuery !== safeQuery) {
348
352
  runScopedFtsQuery(relaxedQuery);
349
353
  }
350
- // Tier 1.5: Fragment graph expansion
351
- const fragmentExpansionDocs = [];
352
- const queryLower = (prompt + " " + keywords).toLowerCase();
353
- const fragmentBoostDocKeys = getEntityBoostDocs(db, queryLower);
354
- for (const docKey of fragmentBoostDocKeys) {
355
- if (ftsSeenKeys.has(docKey))
356
- continue;
357
- const rows = queryDocRows(db, "SELECT project, filename, type, content, path FROM docs WHERE path = ? LIMIT 1", [docKey]);
358
- if (rows?.length) {
359
- ftsSeenKeys.add(docKey);
360
- fragmentExpansionDocs.push(rows[0]);
361
- }
362
- }
363
354
  // Tier 2: Token-overlap semantic — always run, scored independently
364
355
  const semanticDocs = semanticFallbackDocs(db, `${prompt}\n${keywords}`, detectedProject);
365
356
  // Merge with Reciprocal Rank Fusion so documents found by both tiers rank highest
366
- const merged = rrfMerge([ftsDocs, fragmentExpansionDocs, semanticDocs]);
357
+ const merged = rrfMerge([ftsDocs, semanticDocs]);
367
358
  if (merged.length === 0)
368
359
  return null;
369
360
  return merged.slice(0, 12);
@@ -400,8 +391,8 @@ export async function searchDocumentsAsync(db, safeQuery, prompt, keywords, dete
400
391
  }
401
392
  catch (err) {
402
393
  // Vector search failure is non-fatal — return sync result
403
- if (process.env.PHREN_DEBUG)
404
- process.stderr.write(`[phren] hybridSearch vectorFallback: ${err instanceof Error ? err.message : String(err)}\n`);
394
+ if ((process.env.PHREN_DEBUG))
395
+ process.stderr.write(`[phren] hybridSearch vectorFallback: ${errorMessage(err)}\n`);
405
396
  return syncResult;
406
397
  }
407
398
  }
@@ -460,7 +451,7 @@ export async function searchKnowledgeRows(db, options) {
460
451
  }
461
452
  }
462
453
  catch (err) {
463
- debugLog(`rowid dedup query failed: ${err instanceof Error ? err.message : String(err)}`);
454
+ debugLog(`rowid dedup query failed: ${errorMessage(err)}`);
464
455
  }
465
456
  const cosineResults = cosineFallback(db, query, ftsRowids, maxResults - rows.length)
466
457
  .filter((doc) => (!filterProject || doc.project === filterProject) && (!filterType || doc.type === filterType));
@@ -500,8 +491,8 @@ export async function searchKnowledgeRows(db, options) {
500
491
  }
501
492
  }
502
493
  catch (err) {
503
- if (process.env.PHREN_DEBUG) {
504
- process.stderr.write(`[phren] vectorFallback: ${err instanceof Error ? err.message : String(err)}\n`);
494
+ if ((process.env.PHREN_DEBUG)) {
495
+ process.stderr.write(`[phren] vectorFallback: ${errorMessage(err)}\n`);
505
496
  }
506
497
  }
507
498
  }
@@ -515,7 +506,6 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
515
506
  const queueItems = [];
516
507
  const auditEntries = [];
517
508
  const highImpactFindingIds = phrenPath ? getHighImpactFindings(phrenPath, 3) : undefined;
518
- const impactCounts = phrenPath ? getImpactSurfaceCounts(phrenPath, 1) : undefined;
519
509
  const filtered = rows
520
510
  .map((doc) => {
521
511
  if (!TRUST_FILTERED_TYPES.has(doc.type))
@@ -526,7 +516,6 @@ export function applyTrustFilter(rows, ttlDays, minConfidence, decay, phrenPath)
526
516
  decay,
527
517
  project: doc.project,
528
518
  highImpactFindingIds,
529
- impactCounts,
530
519
  });
531
520
  if (trust.issues.length > 0) {
532
521
  const stale = trust.issues.filter((i) => i.reason === "stale").map((i) => i.bullet);
@@ -630,7 +619,7 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
630
619
  const scored = ranked.map((doc) => {
631
620
  const globBoost = getProjectGlobBoost(phrenPathLocal, doc.project, cwd, gitCtx?.changedFiles);
632
621
  const key = entryScoreKey(doc.project, doc.filename, doc.content);
633
- const entity = entityBoostPaths.has(doc.path) ? 1.5 : 1;
622
+ const entity = entityBoostPaths.has(doc.path) ? 1.3 : 1;
634
623
  const date = getRecentDate(doc);
635
624
  const fileRel = fileRelevanceBoost(doc.path, changedFiles);
636
625
  const branchMat = branchMatchBoost(doc.content, gitCtx?.branch);
@@ -645,12 +634,7 @@ export function rankResults(rows, intent, gitCtx, detectedProject, phrenPathLoca
645
634
  && queryOverlap < WEAK_CROSS_PROJECT_OVERLAP_MAX
646
635
  ? WEAK_CROSS_PROJECT_OVERLAP_PENALTY
647
636
  : 0;
648
- // Boost skills whose filename matches a query token (e.g. "swarm" matches swarm.md)
649
- const skillNameBoost = doc.type === "skill" && queryTokens.length > 0
650
- ? queryTokens.some((t) => doc.filename.replace(/\.md$/i, "").toLowerCase() === t) ? 4 : 0
651
- : 0;
652
637
  const score = Math.round((intentBoost(intent, doc.type) +
653
- skillNameBoost +
654
638
  fileRel +
655
639
  branchMat +
656
640
  globBoost +
@@ -754,8 +738,8 @@ export function markStaleCitations(snippet) {
754
738
  }
755
739
  }
756
740
  catch (err) {
757
- if (process.env.PHREN_DEBUG)
758
- process.stderr.write(`[phren] applyCitationAnnotations fileRead: ${err instanceof Error ? err.message : String(err)}\n`);
741
+ if ((process.env.PHREN_DEBUG))
742
+ process.stderr.write(`[phren] applyCitationAnnotations fileRead: ${errorMessage(err)}\n`);
759
743
  stale = true;
760
744
  }
761
745
  }
@@ -771,23 +755,6 @@ export function markStaleCitations(snippet) {
771
755
  }
772
756
  return result.join("\n");
773
757
  }
774
- function annotateContradictions(snippet) {
775
- return snippet.split('\n').map(line => {
776
- const conflictMatch = line.match(/<!-- conflicts_with: "(.*?)" -->/);
777
- const contradictMatch = line.match(/<!-- phren:contradicts "(.*?)" -->/);
778
- const statusMatch = line.match(/phren:status "contradicted"/);
779
- if (conflictMatch) {
780
- return line.replace(conflictMatch[0], '') + ` [CONTRADICTED — conflicts with: "${conflictMatch[1]}"]`;
781
- }
782
- if (contradictMatch) {
783
- return line.replace(contradictMatch[0], '') + ` [CONTRADICTED — see: "${contradictMatch[1]}"]`;
784
- }
785
- if (statusMatch) {
786
- return line + ' [CONTRADICTED]';
787
- }
788
- return line;
789
- }).join('\n');
790
- }
791
758
  export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudget) {
792
759
  const selected = [];
793
760
  let usedTokens = 36;
@@ -813,7 +780,6 @@ export function selectSnippets(rows, keywords, tokenBudget, lineBudget, charBudg
813
780
  if (TRUST_FILTERED_TYPES.has(doc.type)) {
814
781
  snippet = markStaleCitations(snippet);
815
782
  }
816
- snippet = annotateContradictions(snippet);
817
783
  snippet = dedupSnippetBullets(snippet);
818
784
  if (!snippet.trim())
819
785
  continue;
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "crypto";
2
2
  import { debugLog } from "./shared.js";
3
- import { STOP_WORDS } from "./utils.js";
3
+ import { STOP_WORDS, errorMessage } from "./utils.js";
4
4
  import { porterStem } from "./shared-stemmer.js";
5
5
  import { classifyFile, normalizeIndexedContent, rowToDocWithRowid } from "./shared-index.js";
6
6
  import { embedText, cosineSimilarity, getEmbeddingModel, getOllamaUrl, getCloudEmbeddingUrl } from "./shared-ollama.js";
@@ -191,8 +191,8 @@ export function cosineFallback(db, query, excludeRowids, limit) {
191
191
  }
192
192
  }
193
193
  catch (err) {
194
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
195
- process.stderr.write(`[phren] cosineFallback count: ${err instanceof Error ? err.message : String(err)}\n`);
194
+ if ((process.env.PHREN_DEBUG))
195
+ process.stderr.write(`[phren] cosineFallback count: ${errorMessage(err)}\n`);
196
196
  return [];
197
197
  }
198
198
  if (totalDocs > COSINE_MAX_CORPUS) {
@@ -220,8 +220,8 @@ export function cosineFallback(db, query, excludeRowids, limit) {
220
220
  ftsRows.push(...ftsRes[0].values);
221
221
  }
222
222
  catch (err) {
223
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
224
- process.stderr.write(`[phren] cosineFallback FTS pre-filter: ${err instanceof Error ? err.message : String(err)}\n`);
223
+ if ((process.env.PHREN_DEBUG))
224
+ process.stderr.write(`[phren] cosineFallback FTS pre-filter: ${errorMessage(err)}\n`);
225
225
  }
226
226
  }
227
227
  // If FTS gave fewer than cap, supplement with deterministic rowid windows.
@@ -258,8 +258,8 @@ export function cosineFallback(db, query, excludeRowids, limit) {
258
258
  }
259
259
  }
260
260
  catch (err) {
261
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
262
- process.stderr.write(`[phren] cosineFallback deterministicSample: ${err instanceof Error ? err.message : String(err)}\n`);
261
+ if ((process.env.PHREN_DEBUG))
262
+ process.stderr.write(`[phren] cosineFallback deterministicSample: ${errorMessage(err)}\n`);
263
263
  }
264
264
  }
265
265
  if (ftsRows.length === 0)
@@ -269,8 +269,8 @@ export function cosineFallback(db, query, excludeRowids, limit) {
269
269
  }
270
270
  }
271
271
  catch (err) {
272
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
273
- process.stderr.write(`[phren] cosineFallback loadDocs: ${err instanceof Error ? err.message : String(err)}\n`);
272
+ if ((process.env.PHREN_DEBUG))
273
+ process.stderr.write(`[phren] cosineFallback loadDocs: ${errorMessage(err)}\n`);
274
274
  return [];
275
275
  }
276
276
  // Separate rowids, DocRows, and content strings for scoring
@@ -315,8 +315,8 @@ export async function vectorFallback(phrenPath, query, excludePaths, limit, proj
315
315
  await cache.load();
316
316
  }
317
317
  catch (err) {
318
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
319
- process.stderr.write(`[phren] vectorFallback cacheLoad: ${err instanceof Error ? err.message : String(err)}\n`);
318
+ if ((process.env.PHREN_DEBUG))
319
+ process.stderr.write(`[phren] vectorFallback cacheLoad: ${errorMessage(err)}\n`);
320
320
  }
321
321
  }
322
322
  if (cache.size() === 0)
@@ -367,8 +367,8 @@ export async function vectorFallback(phrenPath, query, excludePaths, limit, proj
367
367
  }
368
368
  }
369
369
  catch (err) {
370
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
371
- process.stderr.write(`[phren] vectorFallback fileRead: ${err instanceof Error ? err.message : String(err)}\n`);
370
+ if ((process.env.PHREN_DEBUG))
371
+ process.stderr.write(`[phren] vectorFallback fileRead: ${errorMessage(err)}\n`);
372
372
  }
373
373
  return { project: entryProject, filename, type, content, path: e.path };
374
374
  });
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { createRequire } from "module";
5
+ import { errorMessage } from "./utils.js";
5
6
  const require = createRequire(import.meta.url);
6
7
  /**
7
8
  * Locate the sql.js-fts5 WASM binary by require.resolve with path-probe fallback.
@@ -14,8 +15,8 @@ function findWasmBinary() {
14
15
  return fs.readFileSync(resolved);
15
16
  }
16
17
  catch (err) {
17
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
18
- process.stderr.write(`[phren] findWasmBinary requireResolve: ${err instanceof Error ? err.message : String(err)}\n`);
18
+ if ((process.env.PHREN_DEBUG))
19
+ process.stderr.write(`[phren] findWasmBinary requireResolve: ${errorMessage(err)}\n`);
19
20
  // fall through to path probing
20
21
  }
21
22
  const __filename = fileURLToPath(import.meta.url);
@@ -50,7 +50,7 @@ export function appendAuditLog(phrenPath, event, details) {
50
50
  break;
51
51
  }
52
52
  catch (err) {
53
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
53
+ if ((process.env.PHREN_DEBUG))
54
54
  process.stderr.write(`[phren] appendAuditLog lockWrite: ${errorMessage(err)}\n`);
55
55
  try {
56
56
  const stat = fs.statSync(lockPath);
@@ -68,7 +68,7 @@ export function appendAuditLog(phrenPath, event, details) {
68
68
  }
69
69
  }
70
70
  catch (statErr) {
71
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
71
+ if ((process.env.PHREN_DEBUG))
72
72
  process.stderr.write(`[phren] appendAuditLog staleStat: ${errorMessage(statErr)}\n`);
73
73
  }
74
74
  Atomics.wait(waiter, 0, 0, pollMs);
@@ -97,7 +97,7 @@ export function appendAuditLog(phrenPath, event, details) {
97
97
  fs.unlinkSync(lockPath);
98
98
  }
99
99
  catch (err) {
100
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
100
+ if ((process.env.PHREN_DEBUG))
101
101
  process.stderr.write(`[phren] appendAuditLog unlock: ${errorMessage(err)}\n`);
102
102
  }
103
103
  }
@@ -379,7 +379,7 @@ export async function executePalette(host, input) {
379
379
  }
380
380
  }
381
381
  catch (err) {
382
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
382
+ if ((process.env.PHREN_DEBUG))
383
383
  process.stderr.write(`[phren] shell status gitStatus: ${errorMessage(err)}\n`);
384
384
  }
385
385
  const auditPathNew = runtimeFile(host.phrenPath, "audit.log");
@@ -45,7 +45,7 @@ export function loadShellState(phrenPath) {
45
45
  };
46
46
  }
47
47
  catch (err) {
48
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
48
+ if ((process.env.PHREN_DEBUG))
49
49
  process.stderr.write(`[phren] loadShellState parse: ${errorMessage(err)}\n`);
50
50
  return fallback;
51
51
  }
@@ -13,6 +13,7 @@ import { listMachines, listProfiles, } from "./data-access.js";
13
13
  import { readInstallPreferences } from "./init-preferences.js";
14
14
  import { isProjectHookEnabled, readProjectConfig } from "./project-config.js";
15
15
  import { getScopedSkills } from "./skill-registry.js";
16
+ import { errorMessage } from "./utils.js";
16
17
  // ── Tab bar ────────────────────────────────────────────────────────────────
17
18
  function renderTabBar(state) {
18
19
  const cols = renderWidth();
@@ -248,8 +249,8 @@ function parseSubsections(taskPath, project, cache) {
248
249
  }
249
250
  }
250
251
  catch (err) {
251
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
252
- process.stderr.write(`[phren] buildSubsectionMap: ${err instanceof Error ? err.message : String(err)}\n`);
252
+ if ((process.env.PHREN_DEBUG))
253
+ process.stderr.write(`[phren] buildSubsectionMap: ${errorMessage(err)}\n`);
253
254
  }
254
255
  const newCache = { project, map };
255
256
  return { map, cache: newCache };
package/mcp/dist/shell.js CHANGED
@@ -72,7 +72,7 @@ export class PhrenShell {
72
72
  }
73
73
  }
74
74
  catch (err) {
75
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
75
+ if ((process.env.PHREN_DEBUG))
76
76
  process.stderr.write(`[phren] shell pushUndo: ${errorMessage(err)}\n`);
77
77
  }
78
78
  }
@@ -5,6 +5,7 @@ import { findProjectDir } from "./project-locator.js";
5
5
  import { buildSkillManifest } from "./skill-registry.js";
6
6
  import { setSkillEnabled } from "./skill-state.js";
7
7
  import { errorMessage } from "./utils.js";
8
+ import { isManagedSymlink } from "./link-skills.js";
8
9
  function normalizeSkillRemovalTarget(skillPath) {
9
10
  if (!skillPath)
10
11
  return skillPath;
@@ -19,10 +20,9 @@ function symlinkManagedSkill(src, dest, managedRoot) {
19
20
  if (stat.isSymbolicLink()) {
20
21
  const currentTarget = fs.readlinkSync(dest);
21
22
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
22
- const managedPrefix = path.resolve(managedRoot) + path.sep;
23
23
  if (resolvedTarget === path.resolve(src))
24
24
  return;
25
- if (!resolvedTarget.startsWith(managedPrefix))
25
+ if (!isManagedSymlink(dest, managedRoot))
26
26
  return;
27
27
  fs.unlinkSync(dest);
28
28
  }
@@ -39,18 +39,12 @@ function symlinkManagedSkill(src, dest, managedRoot) {
39
39
  }
40
40
  function removeManagedSkillLink(dest, managedRoot) {
41
41
  try {
42
- const stat = fs.lstatSync(dest);
43
- if (!stat.isSymbolicLink())
44
- return;
45
- const currentTarget = fs.readlinkSync(dest);
46
- const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
47
- const managedPrefix = path.resolve(managedRoot) + path.sep;
48
- if (!resolvedTarget.startsWith(managedPrefix))
42
+ if (!isManagedSymlink(dest, managedRoot))
49
43
  return;
50
44
  fs.unlinkSync(dest);
51
45
  }
52
46
  catch (err) {
53
- if (err.code !== "ENOENT" && (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG)) {
47
+ if (err.code !== "ENOENT" && (process.env.PHREN_DEBUG)) {
54
48
  process.stderr.write(`[phren] removeManagedSkillLink: ${errorMessage(err)}\n`);
55
49
  }
56
50
  }
@@ -64,6 +64,9 @@ function getProjectLocalSkills(phrenPath, project) {
64
64
  return collectSkills(phrenPath, path.join(projectDir, "skills"), project, "project", "canonical", seen);
65
65
  }
66
66
  function skillPriority(skill) {
67
+ // "user" scope (priority 500) reserved for skills the user marks as untouchable —
68
+ // not yet wired to a scopeType value, but the priority slot is established here
69
+ // so the ordering is stable when we add it.
67
70
  if (skill.scopeType === "project" && skill.sourceKind === "canonical")
68
71
  return 400;
69
72
  if (skill.scopeType === "global" && skill.sourceKind === "canonical")
@@ -3,9 +3,10 @@ import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { findPhrenPath, getProjectDirs, EXEC_TIMEOUT_QUICK_MS, debugLog, isRecord, hookConfigPath, homeDir, readRootManifest, } from "./shared.js";
5
5
  import { buildIndex, detectProject, findFtsCacheForPath, listIndexedDocumentPaths, queryRows } from "./shared-index.js";
6
+ import { mergeConfig, getWorkflowPolicy } from "./shared-governance.js";
6
7
  import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init.js";
7
8
  import { getTelemetrySummary } from "./telemetry.js";
8
- import { runGit as runGitShared } from "./utils.js";
9
+ import { runGit as runGitShared, errorMessage } from "./utils.js";
9
10
  import { readRuntimeHealth, resolveTaskFilePath } from "./data-access.js";
10
11
  import { resolveRuntimeProfile } from "./runtime-profile.js";
11
12
  import { renderPhrenArt } from "./phren-art.js";
@@ -17,8 +18,8 @@ function readPackageVersion() {
17
18
  return typeof pkg.version === "string" ? pkg.version : "unknown";
18
19
  }
19
20
  catch (err) {
20
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
21
- process.stderr.write(`[phren] readPackageVersion: ${err instanceof Error ? err.message : String(err)}\n`);
21
+ if ((process.env.PHREN_DEBUG))
22
+ process.stderr.write(`[phren] readPackageVersion: ${errorMessage(err)}\n`);
22
23
  return "unknown";
23
24
  }
24
25
  }
@@ -71,6 +72,33 @@ export async function runStatus() {
71
72
  // Active project
72
73
  if (activeProject) {
73
74
  console.log(` ${DIM}project${RESET} ${activeProject}`);
75
+ // Effective config for this project
76
+ try {
77
+ const resolved = mergeConfig(phrenPath, activeProject);
78
+ const globalWorkflow = getWorkflowPolicy(phrenPath);
79
+ const projectSensitivity = resolved.findingSensitivity !== globalWorkflow.findingSensitivity
80
+ ? `${resolved.findingSensitivity} ${DIM}(project override)${RESET}`
81
+ : resolved.findingSensitivity;
82
+ const projectTaskMode = resolved.taskMode !== globalWorkflow.taskMode
83
+ ? `${resolved.taskMode} ${DIM}(project override)${RESET}`
84
+ : resolved.taskMode;
85
+ console.log(` ${DIM}sensitivity${RESET} ${projectSensitivity}`);
86
+ console.log(` ${DIM}task mode${RESET} ${projectTaskMode}`);
87
+ if (resolved.proactivity.base || resolved.proactivity.findings || resolved.proactivity.tasks) {
88
+ const parts = [];
89
+ if (resolved.proactivity.base)
90
+ parts.push(`base:${resolved.proactivity.base}`);
91
+ if (resolved.proactivity.findings)
92
+ parts.push(`findings:${resolved.proactivity.findings}`);
93
+ if (resolved.proactivity.tasks)
94
+ parts.push(`tasks:${resolved.proactivity.tasks}`);
95
+ console.log(` ${DIM}proactivity${RESET} ${parts.join(" ")} ${DIM}(project override)${RESET}`);
96
+ }
97
+ }
98
+ catch (err) {
99
+ if ((process.env.PHREN_DEBUG))
100
+ process.stderr.write(`[phren] statusConfig: ${errorMessage(err)}\n`);
101
+ }
74
102
  }
75
103
  // Phren path and config
76
104
  console.log(` ${DIM}path${RESET} ${phrenPath}`);
@@ -102,8 +130,8 @@ export async function runStatus() {
102
130
  mcpConfigured = Boolean(servers?.phren || servers?.phren);
103
131
  }
104
132
  catch (err) {
105
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
106
- process.stderr.write(`[phren] statusWorkspaceMcp parse: ${err instanceof Error ? err.message : String(err)}\n`);
133
+ if ((process.env.PHREN_DEBUG))
134
+ process.stderr.write(`[phren] statusWorkspaceMcp parse: ${errorMessage(err)}\n`);
107
135
  }
108
136
  }
109
137
  }
@@ -120,8 +148,8 @@ export async function runStatus() {
120
148
  hooksInstalled = hookEvents.every((event) => hasCommandHook(hooks?.[event]));
121
149
  }
122
150
  catch (err) {
123
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
124
- process.stderr.write(`[phren] statusHooks settingsParse: ${err instanceof Error ? err.message : String(err)}\n`);
151
+ if ((process.env.PHREN_DEBUG))
152
+ process.stderr.write(`[phren] statusHooks settingsParse: ${errorMessage(err)}\n`);
125
153
  }
126
154
  }
127
155
  }
@@ -151,8 +179,8 @@ export async function runStatus() {
151
179
  }
152
180
  }
153
181
  catch (err) {
154
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
155
- process.stderr.write(`[phren] statusFtsIndex: ${err instanceof Error ? err.message : String(err)}\n`);
182
+ if ((process.env.PHREN_DEBUG))
183
+ process.stderr.write(`[phren] statusFtsIndex: ${errorMessage(err)}\n`);
156
184
  }
157
185
  const ftsLabel = ftsIndexOk
158
186
  ? `${GREEN}ok${RESET} ${DIM}(${ftsIndexSize > 0 ? `${(ftsIndexSize / 1024).toFixed(0)} KB` : `${ftsDocCount ?? 0} docs`})${RESET}`
@@ -186,8 +214,8 @@ export async function runStatus() {
186
214
  }
187
215
  }
188
216
  catch (err) {
189
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
190
- process.stderr.write(`[phren] statusSemantic: ${err instanceof Error ? err.message : String(err)}\n`);
217
+ if ((process.env.PHREN_DEBUG))
218
+ process.stderr.write(`[phren] statusSemantic: ${errorMessage(err)}\n`);
191
219
  }
192
220
  // Agent integration status
193
221
  function hasPhrenEntry(filePath) {
@@ -198,8 +226,8 @@ export async function runStatus() {
198
226
  return raw.includes('"phren"') || raw.includes("'phren'") || raw.includes('"phren"') || raw.includes("'phren'");
199
227
  }
200
228
  catch (err) {
201
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
202
- process.stderr.write(`[phren] hasPhrenEntry: ${err instanceof Error ? err.message : String(err)}\n`);
229
+ if ((process.env.PHREN_DEBUG))
230
+ process.stderr.write(`[phren] hasPhrenEntry: ${errorMessage(err)}\n`);
203
231
  return false;
204
232
  }
205
233
  }
@@ -141,7 +141,7 @@ function collectCorpus(root) {
141
141
  texts.push(fs.readFileSync(fullPath, "utf8").slice(0, MAX_TEXT_BYTES).toLowerCase());
142
142
  }
143
143
  catch (err) {
144
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
144
+ if ((process.env.PHREN_DEBUG))
145
145
  process.stderr.write(`[phren] task hygiene read ${fullPath}: ${errorMessage(err)}\n`);
146
146
  }
147
147
  if (filesSeen >= MAX_FILES_PER_ROOT)
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { runtimeDir } from "./shared.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  // In-memory buffers keyed by phrenPath to batch disk writes
5
6
  // Keeping per-path buffers avoids silently losing events when the active path changes.
6
7
  const buffers = new Map();
@@ -25,8 +26,8 @@ function loadFromDisk(phrenPath) {
25
26
  };
26
27
  }
27
28
  catch (err) {
28
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
29
- process.stderr.write(`[phren] telemetry loadFromDisk: ${err instanceof Error ? err.message : String(err)}\n`);
29
+ if ((process.env.PHREN_DEBUG))
30
+ process.stderr.write(`[phren] telemetry loadFromDisk: ${errorMessage(err)}\n`);
30
31
  return defaults;
31
32
  }
32
33
  }
@@ -57,8 +58,8 @@ function flushTelemetryForPath(phrenPath) {
57
58
  fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
58
59
  }
59
60
  catch (err) {
60
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
61
- process.stderr.write(`[phren] telemetry flush: ${err instanceof Error ? err.message : String(err)}\n`);
61
+ if ((process.env.PHREN_DEBUG))
62
+ process.stderr.write(`[phren] telemetry flush: ${errorMessage(err)}\n`);
62
63
  }
63
64
  pendingCounts.set(phrenPath, 0);
64
65
  }
@@ -63,7 +63,7 @@ export async function runPhrenUpdate(opts = {}) {
63
63
  }
64
64
  }
65
65
  catch (err) {
66
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
66
+ if ((process.env.PHREN_DEBUG))
67
67
  process.stderr.write(`[phren] runPhrenUpdate gitStatus: ${errorMessage(err)}\n`);
68
68
  }
69
69
  const pull = run("git", ["pull", "--rebase", "--autostash"], root);
package/mcp/dist/utils.js CHANGED
@@ -12,7 +12,7 @@ function loadSynonymsJson(fileName) {
12
12
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
13
13
  }
14
14
  catch (err) {
15
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
15
+ if ((process.env.PHREN_DEBUG))
16
16
  process.stderr.write(`[phren] ${fileName} load failed: ${err instanceof Error ? err.message : String(err)}\n`);
17
17
  return {};
18
18
  }
@@ -276,7 +276,7 @@ function parseSynonymsYaml(filePath) {
276
276
  return loaded;
277
277
  }
278
278
  catch (err) {
279
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
279
+ if ((process.env.PHREN_DEBUG))
280
280
  process.stderr.write(`[phren] synonyms.yaml parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}\n`);
281
281
  return {};
282
282
  }
@@ -315,7 +315,7 @@ function parseLearnedSynonymsJson(filePath) {
315
315
  return loaded;
316
316
  }
317
317
  catch (err) {
318
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
318
+ if ((process.env.PHREN_DEBUG))
319
319
  process.stderr.write(`[phren] learned-synonyms parse failed (${filePath}): ${err instanceof Error ? err.message : String(err)}\n`);
320
320
  return {};
321
321
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.9",
4
- "description": "Knowledge layer for AI agents. Claude remembers you. Phren remembers your work.",
3
+ "version": "0.0.11",
4
+ "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "phren": "mcp/dist/index.js"