@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
@@ -5,110 +5,128 @@ import { errorMessage } from "./utils.js";
5
5
  import { defaultPhrenPath, findPhrenPath } from "./shared.js";
6
6
  import { addProjectFromPath } from "./core-project.js";
7
7
  import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, } from "./project-config.js";
8
- const HELP_TEXT = `phren - He remembers so your agent doesn't have to
8
+ const HELP_TEXT = `phren - persistent knowledge for your agents
9
9
 
10
- Usage:
11
- phren Open interactive shell
12
- phren quickstart Quick setup: init + project scaffold
13
- phren add [path] [--ownership <mode>] Add current directory (or path) as a project
14
- phren init [--mode shared|project-local] [--machine <n>] [--profile <n>] [--mcp on|off] [--template <t>] [--dry-run] [-y]
15
- Set up phren and offer to add the current project directory
16
- phren projects list List all tracked projects
10
+ phren Interactive shell
11
+ phren init Set up phren
12
+ phren quickstart Quick setup: init + project scaffold
13
+ phren add [path] Register a project
14
+ phren search <query> Search what phren knows
15
+ phren status Health check
16
+ phren doctor [--fix] Diagnose and repair
17
+ phren web-ui Open the knowledge graph
18
+ phren tasks Cross-project task view
19
+ phren graph Fragment knowledge graph
20
+
21
+ phren help <topic> Detailed help for a topic
22
+
23
+ Topics: projects, skills, hooks, config, maintain, setup, env, all
24
+ `;
25
+ const HELP_TOPICS = {
26
+ projects: `Projects:
27
+ phren add [path] [--ownership <mode>] Register a project
28
+ phren projects list List all tracked projects
17
29
  phren projects configure <name> [--ownership <mode>] [--hooks on|off]
18
- Update per-project enrollment and hook settings
19
- phren projects remove <name> Remove a project (asks for confirmation)
20
- phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
21
- phren skills list List installed skills
22
- phren skills add <project> <path> Link or copy a skill file into one project
23
- phren skills resolve <project|global> Print the resolved skill manifest for one scope
24
- phren skills doctor <project|global> Diagnose resolved skill visibility + mirror state
25
- phren skills sync <project|global> Regenerate the resolved mirror for one scope
26
- phren skills remove <project> <name> Remove a project skill by name
27
- phren hooks list [--project <name>] Show hook tool preferences and optional project overrides
28
- phren hooks enable <tool> Enable hooks for one tool
29
- phren hooks disable <tool> Disable hooks for one tool
30
- phren status Health, active project, stats
31
- phren review [project] Show review queue items with date, confidence, text
32
- phren consolidation-status [project] Check if findings need consolidation
33
- phren session-context Show current session state (project, duration, findings)
34
- phren search <query> [--project <n>] [--type <t>] [--limit <n>]
35
- Search what phren remembers
36
- phren add-finding <project> "..." Tell phren what you learned
37
- phren pin <project> "..." Save a truth
38
- phren tasks Cross-project task view
39
- phren skill-list List installed skills
40
- phren doctor [--fix] [--check-data] [--agents]
41
- Health check and self-heal (--agents: show agent integrations only)
42
- phren web-ui [--port=3499] [--no-open] Memory web UI
43
- phren debug-injection --prompt "..." Preview hook-prompt injection output
44
- phren inspect-index [--project <n>] Inspect FTS index contents for debugging
45
- phren update [--refresh-starter] Update to latest version
46
- phren graph [--project <n>] [--limit <n>]
47
- Show the fragment knowledge graph
48
- phren graph link <project> "finding" "fragment"
49
- Link a finding to a fragment manually
30
+ Update per-project settings
31
+ phren projects remove <name> Remove a project
32
+ `,
33
+ skills: `Skills:
34
+ phren skills list List installed skills
35
+ phren skills add <project> <path> Link a skill into a project
36
+ phren skills show <name> Show skill content
37
+ phren skills resolve <project|global> Print resolved skill manifest
38
+ phren skills doctor <project|global> Diagnose skill visibility
39
+ phren skills sync <project|global> Regenerate skill mirror
40
+ phren skills enable <project|global> <name> Enable a disabled skill
41
+ phren skills disable <project|global> <name> Disable a skill without deleting
42
+ phren skills remove <project> <name> Remove a skill
43
+ phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
44
+ `,
45
+ hooks: `Hooks:
46
+ phren hooks list [--project <name>] Show hook status per tool
47
+ phren hooks enable <tool> Enable hooks for a tool
48
+ phren hooks disable <tool> Disable hooks for a tool
49
+ phren hooks add-custom <event> <cmd> Add a custom hook
50
+ phren hooks remove-custom <event> Remove custom hooks
51
+ phren hooks errors [--limit <n>] Show recent hook errors
52
+ `,
53
+ config: `Configuration:
54
+ phren config show [--project <name>] Show current config
55
+ phren config policy [get|set ...] Retention, TTL, confidence, decay
56
+ phren config workflow [get|set ...] Risky-memory thresholds
57
+ phren config proactivity [level] Set proactivity level
58
+ phren config task-mode [mode] Set task automation mode
59
+ phren config finding-sensitivity [lvl] Set finding capture sensitivity
60
+ phren config index [get|set ...] Indexer include/exclude globs
61
+ phren config synonyms [list|add|remove] Manage learned synonyms
62
+ phren config project-ownership [mode] Default ownership for new projects
63
+ phren config machines Registered machines
64
+ phren config profiles Profiles and projects
65
+ phren config telemetry [on|off] Opt-in usage telemetry
50
66
 
51
- Configuration:
52
- phren config policy [get|set ...] Retention, TTL, confidence, decay
53
- phren config workflow [get|set ...] Risky-memory thresholds
54
- phren config index [get|set ...] Indexer include/exclude globs
55
- phren config synonyms [list|add|remove] ...
56
- Manage project learned synonyms
57
- phren config project-ownership [mode] Default ownership for future project enrollments
58
- phren config machines Registered machines
59
- phren config profiles Profiles and projects
67
+ All config subcommands accept --project <name> for per-project overrides.
68
+ `,
69
+ maintain: `Maintenance:
70
+ phren maintain govern [project] Queue stale memories for review
71
+ phren maintain prune [project] Delete expired entries
72
+ phren maintain consolidate [project] Deduplicate findings
73
+ phren maintain extract [project] Mine git/GitHub signals
74
+ `,
75
+ setup: `Setup:
76
+ phren init [--mode shared|project-local] [--machine <n>] [--profile <n>] [--dry-run] [-y]
77
+ phren quickstart Quick setup: init + project scaffold
78
+ phren mcp-mode [on|off|status] Toggle MCP integration
79
+ phren hooks-mode [on|off|status] Toggle hook execution
80
+ phren verify Check init completed OK
81
+ phren uninstall Remove phren config and hooks
82
+ phren update [--refresh-starter] Update to latest version
83
+ `,
84
+ env: `Environment variables:
85
+ PHREN_PATH Override phren directory (default: ~/.phren)
86
+ PHREN_PROFILE Active profile name
87
+ PHREN_DEBUG Enable debug logging (set to 1)
60
88
 
61
- Maintenance:
62
- phren maintain govern [project] Queue stale/low-value memories for review
63
- phren maintain prune [project] Delete expired entries
64
- phren maintain consolidate [project] Deduplicate FINDINGS.md
65
- phren maintain extract [project] Mine git/GitHub signals
89
+ Embeddings:
90
+ PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434, 'off' to disable)
91
+ PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint
92
+ PHREN_EMBEDDING_API_KEY API key for embedding endpoint
93
+ PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
66
94
 
67
- Setup:
68
- phren mcp-mode [on|off|status] Toggle MCP integration
69
- phren hooks-mode [on|off|status] Toggle hook execution
70
- phren verify Check init completed OK
71
- phren uninstall Remove phren config and hooks
95
+ Context injection:
96
+ PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per prompt (default: 550)
97
+ PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
98
+ PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
72
99
 
73
- Environment:
74
- PHREN_PATH Override phren directory (default: ~/.phren)
75
- PHREN_PROFILE Active profile name (otherwise phren uses machines.yaml when available)
76
- PHREN_DEBUG Enable debug logging (set to 1)
77
- PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434; set to 'off' to disable)
78
- PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint (cloud alternative to Ollama)
79
- PHREN_EMBEDDING_API_KEY API key for PHREN_EMBEDDING_API_URL
80
- PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
81
- PHREN_EXTRACT_MODEL Ollama model for memory extraction (default: llama3.2)
82
- PHREN_EMBEDDING_PROVIDER Set to 'api' to use OpenAI API for search_knowledge embeddings
83
- PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations at session end
84
- PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
85
- PHREN_FEATURE_SEMANTIC_CONFLICT=1 LLM-based conflict detection on add_finding
86
- PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback in search_knowledge
87
- PHREN_FEATURE_AUTO_EXTRACT=0 Disable automatic memory extraction on each prompt
88
- PHREN_FEATURE_PROGRESSIVE_DISCLOSURE=1 Compact memory index injection
89
- PHREN_LLM_MODEL LLM model for semantic dedup/conflict (default: gpt-4o-mini)
90
- PHREN_LLM_ENDPOINT OpenAI-compatible /chat/completions base URL for dedup/conflict
91
- PHREN_LLM_KEY API key for PHREN_LLM_ENDPOINT
92
- PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per hook-prompt (default: 550)
93
- PHREN_CONTEXT_SNIPPET_LINES Max lines per injected snippet (default: 6)
94
- PHREN_CONTEXT_SNIPPET_CHARS Max chars per injected snippet (default: 520)
95
- PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
96
- PHREN_TASK_PRIORITY Priorities to include in task injection: high,medium,low (default: high,medium)
97
- PHREN_MEMORY_TTL_DAYS Override memory TTL for trust filtering
98
- PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
99
- PHREN_FINDINGS_CAP Max findings per date section before consolidation (default: 20)
100
- PHREN_GH_PR_LIMIT/RUN_LIMIT/ISSUE_LIMIT GitHub extraction limits (defaults: 40/25/25)
100
+ Feature flags:
101
+ PHREN_FEATURE_AUTO_EXTRACT=0 Disable auto memory extraction
102
+ PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations
103
+ PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
104
+ PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback
101
105
 
102
- Examples:
103
- phren search "rate limiting" Search across all projects
104
- phren search "auth" --project my-api Search within one project
105
- phren add-finding my-app "Redis connections need explicit close in finally blocks"
106
- phren doctor --fix Fix common config issues
107
- phren config policy set --ttlDays=90 Change memory retention to 90 days
108
- phren config project-ownership detached
109
- phren maintain govern my-app Queue stale memories for review
110
- phren status Quick health check
111
- `;
106
+ Run 'phren help all' to see everything.
107
+ `,
108
+ };
109
+ function buildFullHelp() {
110
+ return `phren - persistent knowledge for your agents
111
+
112
+ Usage:
113
+ phren Interactive shell
114
+ phren init Set up phren
115
+ phren quickstart Quick setup: init + project scaffold
116
+ phren add [path] Register a project
117
+ phren search <query> Search what phren knows
118
+ phren status Health check
119
+ phren doctor [--fix] Diagnose and repair
120
+ phren web-ui Open the knowledge graph
121
+ phren tasks Cross-project task view
122
+ phren graph Fragment knowledge graph
123
+ phren add-finding <p> "." Tell phren what you learned
124
+ phren pin <p> "..." Save a truth
125
+ phren review [project] Show review queue
126
+ phren session-context Current session state
127
+
128
+ ${Object.values(HELP_TOPICS).join("\n")}`;
129
+ }
112
130
  const CLI_COMMANDS = [
113
131
  "search",
114
132
  "shell",
@@ -216,7 +234,19 @@ async function promptProjectOwnership(phrenPath, fallback) {
216
234
  export async function runTopLevelCommand(argv) {
217
235
  const argvCommand = argv[0];
218
236
  if (argvCommand === "--help" || argvCommand === "-h" || argvCommand === "help") {
219
- console.log(HELP_TEXT);
237
+ const topic = argv[1]?.toLowerCase();
238
+ if (topic === "all") {
239
+ console.log(buildFullHelp());
240
+ }
241
+ else if (topic && HELP_TOPICS[topic]) {
242
+ console.log(HELP_TOPICS[topic]);
243
+ }
244
+ else if (topic) {
245
+ console.log(`Unknown topic: ${topic}\nAvailable: ${Object.keys(HELP_TOPICS).join(", ")}, all`);
246
+ }
247
+ else {
248
+ console.log(HELP_TEXT);
249
+ }
220
250
  return finish();
221
251
  }
222
252
  if (argvCommand === "add") {
@@ -313,7 +343,8 @@ export async function runTopLevelCommand(argv) {
313
343
  }
314
344
  if (argvCommand === "uninstall") {
315
345
  const { runUninstall } = await import("./init.js");
316
- await runUninstall();
346
+ const skipConfirm = argv.includes("--yes") || argv.includes("-y");
347
+ await runUninstall({ yes: skipConfirm });
317
348
  return finish();
318
349
  }
319
350
  if (argvCommand === "status") {
@@ -349,7 +380,7 @@ export async function runTopLevelCommand(argv) {
349
380
  return finish();
350
381
  }
351
382
  catch (err) {
352
- console.error(err instanceof Error ? err.message : String(err));
383
+ console.error(errorMessage(err));
353
384
  return finish(1);
354
385
  }
355
386
  }
@@ -360,7 +391,7 @@ export async function runTopLevelCommand(argv) {
360
391
  return finish();
361
392
  }
362
393
  catch (err) {
363
- console.error(err instanceof Error ? err.message : String(err));
394
+ console.error(errorMessage(err));
364
395
  return finish(1);
365
396
  }
366
397
  }
@@ -383,7 +414,7 @@ export async function runTopLevelCommand(argv) {
383
414
  trackCliCommand(defaultPhrenPath(), argvCommand);
384
415
  }
385
416
  catch (err) {
386
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
417
+ if ((process.env.PHREN_DEBUG))
387
418
  process.stderr.write(`[phren] cli trackCliCommand: ${errorMessage(err)}\n`);
388
419
  }
389
420
  await runCliCommand(argvCommand, argv.slice(1));
@@ -2,19 +2,11 @@ import * as crypto from "crypto";
2
2
  import * as fs from "fs";
3
3
  import { impactLogFile } from "./shared.js";
4
4
  import { withFileLock } from "./shared-governance.js";
5
+ import { normalizeFindingText } from "./content-metadata.js";
5
6
  let highImpactCache = null;
6
7
  function nowIso() {
7
8
  return new Date().toISOString();
8
9
  }
9
- function normalizeFindingText(raw) {
10
- return raw
11
- .replace(/^-\s+/, "")
12
- .replace(/<!--.*?-->/g, " ")
13
- .replace(/\[confidence\s+[01](?:\.\d+)?\]/gi, " ")
14
- .replace(/\s+/g, " ")
15
- .trim()
16
- .toLowerCase();
17
- }
18
10
  export function findingIdFromLine(line) {
19
11
  const fid = line.match(/<!--\s*fid:([a-z0-9]{8})\s*-->/i);
20
12
  if (fid?.[1])
@@ -144,29 +136,6 @@ export function getHighImpactFindings(phrenPath, minSurfaceCount = 3) {
144
136
  };
145
137
  return new Set(ids);
146
138
  }
147
- export function getImpactSurfaceCounts(phrenPath, minSurfaces = 1) {
148
- const file = impactLogFile(phrenPath);
149
- if (!fs.existsSync(file))
150
- return new Map();
151
- const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
152
- const counts = new Map();
153
- for (const line of lines) {
154
- try {
155
- const entry = JSON.parse(line);
156
- if (entry.findingId) {
157
- counts.set(entry.findingId, (counts.get(entry.findingId) ?? 0) + 1);
158
- }
159
- }
160
- catch { }
161
- }
162
- // Filter by minimum
163
- const filtered = new Map();
164
- for (const [id, count] of counts) {
165
- if (count >= minSurfaces)
166
- filtered.set(id, count);
167
- }
168
- return filtered;
169
- }
170
139
  export function markImpactEntriesCompletedForSession(phrenPath, sessionId, project) {
171
140
  if (!sessionId)
172
141
  return 0;
@@ -105,7 +105,7 @@ export function compactFindingJournals(phrenPath, project) {
105
105
  result.failed += 1;
106
106
  continue;
107
107
  }
108
- if (typeof write.data === "string" && write.data.includes("Skipped duplicate"))
108
+ if (write.data.status === "skipped")
109
109
  result.skipped += 1;
110
110
  else
111
111
  result.added += 1;
@@ -3,9 +3,9 @@ import * as path from "path";
3
3
  import { PhrenError, phrenErr, phrenOk } from "./phren-core.js";
4
4
  // Phren lifecycle comment prefix. No backward compat.
5
5
  const LIFECYCLE_PREFIX = "phren";
6
- import { withFileLock } from "./governance-locks.js";
6
+ import { withFileLock } from "./shared-governance.js";
7
7
  import { isValidProjectName, safeProjectPath } from "./utils.js";
8
- import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, } from "./content-metadata.js";
8
+ import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "./content-metadata.js";
9
9
  export const FINDING_TYPE_DECAY = {
10
10
  'pattern': { maxAgeDays: 365, decayMultiplier: 1.0 }, // Slow decay, long-lived
11
11
  'decision': { maxAgeDays: Infinity, decayMultiplier: 1.0 }, // Never decays
@@ -101,11 +101,6 @@ function findingTextFromLine(line) {
101
101
  .replace(/<!--.*?-->/g, "")
102
102
  .trim();
103
103
  }
104
- function normalizeFindingText(value) {
105
- return findingTextFromLine(value)
106
- .replace(/\s+/g, " ")
107
- .toLowerCase();
108
- }
109
104
  function removeRelationComments(line) {
110
105
  return stripRelationMetadata(line);
111
106
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { debugLog } from "./shared.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  // Acquire the file lock, returning true on success or throwing on timeout.
5
6
  function acquireFileLock(lockPath) {
6
7
  const maxWait = Number.parseInt(process.env.PHREN_FILE_LOCK_MAX_WAIT_MS || "5000", 10) || 5000;
@@ -18,8 +19,8 @@ function acquireFileLock(lockPath) {
18
19
  break;
19
20
  }
20
21
  catch (err) {
21
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
22
- process.stderr.write(`[phren] acquireFileLock lockWrite: ${err instanceof Error ? err.message : String(err)}\n`);
22
+ if ((process.env.PHREN_DEBUG))
23
+ process.stderr.write(`[phren] acquireFileLock lockWrite: ${errorMessage(err)}\n`);
23
24
  try {
24
25
  const stat = fs.statSync(lockPath);
25
26
  if (Date.now() - stat.mtimeMs > staleThreshold) {
@@ -53,7 +54,7 @@ function acquireFileLock(lockPath) {
53
54
  }
54
55
  }
55
56
  catch (statErr) {
56
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
57
+ if ((process.env.PHREN_DEBUG))
57
58
  process.stderr.write(`[phren] acquireFileLock staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
58
59
  sleep(pollInterval);
59
60
  waited += pollInterval;
@@ -74,8 +75,8 @@ function releaseFileLock(lockPath) {
74
75
  fs.unlinkSync(lockPath);
75
76
  }
76
77
  catch (err) {
77
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
78
- process.stderr.write(`[phren] releaseFileLock: ${err instanceof Error ? err.message : String(err)}\n`);
78
+ if ((process.env.PHREN_DEBUG))
79
+ process.stderr.write(`[phren] releaseFileLock: ${errorMessage(err)}\n`);
79
80
  }
80
81
  }
81
82
  // Q10: withFileLock now accepts both sync and async callbacks.
@@ -99,3 +100,9 @@ export function withFileLock(filePath, fn) {
99
100
  releaseFileLock(lockPath);
100
101
  return result;
101
102
  }
103
+ export function isFiniteNumber(value) {
104
+ return typeof value === "number" && Number.isFinite(value);
105
+ }
106
+ export function hasValidSchemaVersion(data) {
107
+ return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
108
+ }
@@ -2,8 +2,9 @@ import * as crypto from "crypto";
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
  import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "./shared.js";
5
- import { withFileLock } from "./governance-locks.js";
5
+ import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.js";
6
6
  import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
7
+ import { readProjectConfig } from "./project-config.js";
7
8
  import { runCustomHooks } from "./hooks.js";
8
9
  import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "./content-metadata.js";
9
10
  export const MAX_QUEUE_ENTRY_LENGTH = 500;
@@ -43,15 +44,9 @@ function governanceDir(phrenPath) {
43
44
  function govFile(phrenPath, schema) {
44
45
  return path.join(governanceDir(phrenPath), GOVERNANCE_REGISTRY[schema].file);
45
46
  }
46
- function hasValidSchemaVersion(data) {
47
- return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
48
- }
49
47
  function isStringArray(value) {
50
48
  return Array.isArray(value) && value.every((item) => typeof item === "string");
51
49
  }
52
- function isFiniteNumber(value) {
53
- return typeof value === "number" && Number.isFinite(value);
54
- }
55
50
  function pickNumber(value, fallback) {
56
51
  return isFiniteNumber(value) ? value : fallback;
57
52
  }
@@ -212,6 +207,148 @@ function normalizeIndexPolicy(data) {
212
207
  includeHidden: pickBoolean(data.includeHidden, DEFAULT_INDEX_POLICY.includeHidden),
213
208
  };
214
209
  }
210
+ const VALID_PROACTIVITY_LEVELS = ["high", "medium", "low"];
211
+ export const VALID_TASK_MODES = ["off", "manual", "suggest", "auto"];
212
+ export const VALID_FINDING_SENSITIVITY = ["minimal", "conservative", "balanced", "aggressive"];
213
+ const VALID_RISKY_SECTIONS = ["Review", "Stale", "Conflicts"];
214
+ function pickEnum(value, allowed) {
215
+ return typeof value === "string" && allowed.includes(value) ? value : undefined;
216
+ }
217
+ function pickPositiveInt(value) {
218
+ return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
219
+ }
220
+ function pickUnitInterval(value) {
221
+ return isFiniteNumber(value) && value >= 0 && value <= 1 ? value : undefined;
222
+ }
223
+ function normalizeProjectConfigOverrides(raw) {
224
+ if (!isRecord(raw))
225
+ return undefined;
226
+ const retentionRaw = isRecord(raw.retentionPolicy) ? raw.retentionPolicy : undefined;
227
+ const decayRaw = retentionRaw && isRecord(retentionRaw.decay) ? retentionRaw.decay : undefined;
228
+ const retentionPolicy = retentionRaw
229
+ ? {
230
+ ttlDays: pickPositiveInt(retentionRaw.ttlDays),
231
+ retentionDays: pickPositiveInt(retentionRaw.retentionDays),
232
+ autoAcceptThreshold: pickUnitInterval(retentionRaw.autoAcceptThreshold),
233
+ minInjectConfidence: pickUnitInterval(retentionRaw.minInjectConfidence),
234
+ decay: decayRaw
235
+ ? {
236
+ d30: pickUnitInterval(decayRaw.d30),
237
+ d60: pickUnitInterval(decayRaw.d60),
238
+ d90: pickUnitInterval(decayRaw.d90),
239
+ d120: pickUnitInterval(decayRaw.d120),
240
+ }
241
+ : undefined,
242
+ }
243
+ : undefined;
244
+ if (retentionPolicy && retentionPolicy.decay && Object.values(retentionPolicy.decay).every((value) => value === undefined)) {
245
+ delete retentionPolicy.decay;
246
+ }
247
+ const workflowRaw = isRecord(raw.workflowPolicy) ? raw.workflowPolicy : undefined;
248
+ const workflowPolicy = workflowRaw
249
+ ? {
250
+ lowConfidenceThreshold: pickUnitInterval(workflowRaw.lowConfidenceThreshold),
251
+ riskySections: Array.isArray(workflowRaw.riskySections)
252
+ ? workflowRaw.riskySections.filter((section) => typeof section === "string" && VALID_RISKY_SECTIONS.includes(section))
253
+ : undefined,
254
+ }
255
+ : undefined;
256
+ if (workflowPolicy && workflowPolicy.riskySections && workflowPolicy.riskySections.length === 0) {
257
+ delete workflowPolicy.riskySections;
258
+ }
259
+ const overrides = {
260
+ findingSensitivity: pickEnum(raw.findingSensitivity, VALID_FINDING_SENSITIVITY),
261
+ proactivity: pickEnum(raw.proactivity, VALID_PROACTIVITY_LEVELS),
262
+ proactivityFindings: pickEnum(raw.proactivityFindings, VALID_PROACTIVITY_LEVELS),
263
+ proactivityTask: pickEnum(raw.proactivityTask, VALID_PROACTIVITY_LEVELS),
264
+ taskMode: pickEnum(raw.taskMode, VALID_TASK_MODES),
265
+ retentionPolicy: retentionPolicy && Object.values(retentionPolicy).some((value) => value !== undefined)
266
+ ? retentionPolicy
267
+ : undefined,
268
+ workflowPolicy: workflowPolicy && Object.values(workflowPolicy).some((value) => value !== undefined)
269
+ ? workflowPolicy
270
+ : undefined,
271
+ };
272
+ return overrides;
273
+ }
274
+ function readProjectConfigOverrides(phrenPath, projectName) {
275
+ try {
276
+ const config = readProjectConfig(phrenPath, projectName);
277
+ return normalizeProjectConfigOverrides(config.config);
278
+ }
279
+ catch {
280
+ return undefined;
281
+ }
282
+ }
283
+ export function getProjectConfigOverrides(phrenPath, projectName) {
284
+ return readProjectConfigOverrides(phrenPath, projectName) ?? null;
285
+ }
286
+ export function mergeConfig(phrenPath, projectName) {
287
+ const globalRetention = getRetentionPolicyGlobal(phrenPath);
288
+ const globalWorkflow = getWorkflowPolicyGlobal(phrenPath);
289
+ if (projectName && !isValidProjectName(projectName)) {
290
+ debugLog(`mergeConfig: invalid project name "${projectName}", using global defaults`);
291
+ projectName = undefined;
292
+ }
293
+ if (!projectName) {
294
+ return {
295
+ findingSensitivity: globalWorkflow.findingSensitivity,
296
+ proactivity: {},
297
+ taskMode: globalWorkflow.taskMode,
298
+ retentionPolicy: globalRetention,
299
+ workflowPolicy: globalWorkflow,
300
+ };
301
+ }
302
+ const overrides = readProjectConfigOverrides(phrenPath, projectName);
303
+ if (!overrides) {
304
+ return {
305
+ findingSensitivity: globalWorkflow.findingSensitivity,
306
+ proactivity: {},
307
+ taskMode: globalWorkflow.taskMode,
308
+ retentionPolicy: globalRetention,
309
+ workflowPolicy: globalWorkflow,
310
+ };
311
+ }
312
+ // Merge retention policy
313
+ const retentionOverride = overrides.retentionPolicy;
314
+ const mergedRetention = retentionOverride
315
+ ? {
316
+ schemaVersion: globalRetention.schemaVersion,
317
+ ttlDays: retentionOverride.ttlDays ?? globalRetention.ttlDays,
318
+ retentionDays: retentionOverride.retentionDays ?? globalRetention.retentionDays,
319
+ autoAcceptThreshold: retentionOverride.autoAcceptThreshold ?? globalRetention.autoAcceptThreshold,
320
+ minInjectConfidence: retentionOverride.minInjectConfidence ?? globalRetention.minInjectConfidence,
321
+ decay: {
322
+ d30: retentionOverride.decay?.d30 ?? globalRetention.decay.d30,
323
+ d60: retentionOverride.decay?.d60 ?? globalRetention.decay.d60,
324
+ d90: retentionOverride.decay?.d90 ?? globalRetention.decay.d90,
325
+ d120: retentionOverride.decay?.d120 ?? globalRetention.decay.d120,
326
+ },
327
+ }
328
+ : globalRetention;
329
+ // Merge workflow policy
330
+ const workflowOverride = overrides.workflowPolicy;
331
+ const mergedWorkflow = {
332
+ schemaVersion: globalWorkflow.schemaVersion,
333
+ lowConfidenceThreshold: workflowOverride?.lowConfidenceThreshold ?? globalWorkflow.lowConfidenceThreshold,
334
+ riskySections: workflowOverride?.riskySections?.length
335
+ ? workflowOverride.riskySections
336
+ : globalWorkflow.riskySections,
337
+ taskMode: overrides.taskMode ?? globalWorkflow.taskMode,
338
+ findingSensitivity: overrides.findingSensitivity ?? globalWorkflow.findingSensitivity,
339
+ };
340
+ return {
341
+ findingSensitivity: mergedWorkflow.findingSensitivity,
342
+ proactivity: {
343
+ base: overrides.proactivity,
344
+ findings: overrides.proactivityFindings,
345
+ tasks: overrides.proactivityTask,
346
+ },
347
+ taskMode: mergedWorkflow.taskMode,
348
+ retentionPolicy: mergedRetention,
349
+ workflowPolicy: mergedWorkflow,
350
+ };
351
+ }
215
352
  function readJsonFile(filePath, fallback) {
216
353
  try {
217
354
  if (!fs.existsSync(filePath))
@@ -246,10 +383,15 @@ function writeJsonFile(filePath, data) {
246
383
  writeJsonFileUnlocked(filePath, data);
247
384
  });
248
385
  }
249
- export function getRetentionPolicy(phrenPath) {
386
+ function getRetentionPolicyGlobal(phrenPath) {
250
387
  const parsed = readJsonFile(govFile(phrenPath, "retention-policy"), {});
251
388
  return withDefaults(parsed, DEFAULT_POLICY);
252
389
  }
390
+ export function getRetentionPolicy(phrenPath, projectName) {
391
+ if (projectName)
392
+ return mergeConfig(phrenPath, projectName).retentionPolicy;
393
+ return getRetentionPolicyGlobal(phrenPath);
394
+ }
253
395
  export function updateRetentionPolicy(phrenPath, patch) {
254
396
  const current = getRetentionPolicy(phrenPath);
255
397
  const next = {
@@ -264,7 +406,7 @@ export function updateRetentionPolicy(phrenPath, patch) {
264
406
  appendAuditLog(phrenPath, "update_policy", JSON.stringify(next));
265
407
  return phrenOk(next);
266
408
  }
267
- export function getWorkflowPolicy(phrenPath) {
409
+ function getWorkflowPolicyGlobal(phrenPath) {
268
410
  const parsed = readJsonFile(govFile(phrenPath, "workflow-policy"), {});
269
411
  const merged = withDefaults(parsed, DEFAULT_WORKFLOW_POLICY);
270
412
  const validSections = new Set(["Review", "Stale", "Conflicts"]);
@@ -279,6 +421,11 @@ export function getWorkflowPolicy(phrenPath) {
279
421
  }
280
422
  return merged;
281
423
  }
424
+ export function getWorkflowPolicy(phrenPath, projectName) {
425
+ if (projectName)
426
+ return mergeConfig(phrenPath, projectName).workflowPolicy;
427
+ return getWorkflowPolicyGlobal(phrenPath);
428
+ }
282
429
  export function updateWorkflowPolicy(phrenPath, patch) {
283
430
  const current = getWorkflowPolicy(phrenPath);
284
431
  const riskySections = Array.isArray(patch.riskySections)