@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
package/mcp/dist/link.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import * as crypto from "crypto";
4
3
  import * as readline from "readline";
5
4
  import * as yaml from "js-yaml";
6
5
  import { execFileSync } from "child_process";
7
- import { fileURLToPath } from "url";
8
- import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
6
+ import { ROOT } from "./package-metadata.js";
7
+ import { configureMcpTargets, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
9
8
  import { configureAllHooks, detectInstalledTools } from "./hooks.js";
10
9
  import { getMachineName, persistMachineName } from "./machine-identity.js";
11
- import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, } from "./shared.js";
10
+ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "./shared.js";
12
11
  import { errorMessage } from "./utils.js";
12
+ import { log } from "./init-shared.js";
13
13
  import { listMachines as listMachinesShared, listProfiles as listProfilesShared, setMachineProfile, } from "./profile-store.js";
14
- import { writeSkillMd } from "./link-skills.js";
14
+ import { writeSkillMd, isManagedSymlink } from "./link-skills.js";
15
15
  import { syncScopeSkillsToDir } from "./skill-files.js";
16
16
  import { renderSkillInstructionsSection } from "./skill-registry.js";
17
17
  import { findProjectDir } from "./project-locator.js";
@@ -23,14 +23,6 @@ export { updateFileChecksums, verifyFileChecksums } from "./link-checksums.js";
23
23
  export { findProjectDir } from "./project-locator.js";
24
24
  export { parseSkillFrontmatter, validateSkillFrontmatter, validateSkillsDir, readSkillManifestHooks, } from "./link-skills.js";
25
25
  // ── Helpers (exported for link-doctor) ──────────────────────────────────────
26
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
- function log(msg) { process.stdout.write(msg + "\n"); }
28
- function atomicWriteText(filePath, content) {
29
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
31
- fs.writeFileSync(tmpPath, content);
32
- fs.renameSync(tmpPath, filePath);
33
- }
34
26
  export { getMachineName } from "./machine-identity.js";
35
27
  export function lookupProfile(phrenPath, machine) {
36
28
  const listed = listMachinesShared(phrenPath);
@@ -42,7 +34,7 @@ function listProfiles(phrenPath) {
42
34
  const listed = listProfilesShared(phrenPath);
43
35
  if (!listed.ok)
44
36
  return [];
45
- return listed.data.map((profile) => ({ name: profile.name, description: "" }));
37
+ return listed.data.map((profile) => ({ name: profile.name }));
46
38
  }
47
39
  export function findProfileFile(phrenPath, profileName) {
48
40
  const profilesDir = path.join(phrenPath, "profiles");
@@ -102,7 +94,7 @@ async function registerMachine(phrenPath) {
102
94
  }
103
95
  log("\nAvailable profiles:");
104
96
  for (const p of listProfiles(phrenPath))
105
- log(` ${p.name} (${p.description})`);
97
+ log(` ${p.name}`);
106
98
  log("");
107
99
  const profile = (await ask("Which profile? ")).trim();
108
100
  rl.close();
@@ -123,7 +115,7 @@ function setupSparseCheckout(phrenPath, projects) {
123
115
  execFileSync("git", ["rev-parse", "--git-dir"], { cwd: phrenPath, stdio: "ignore", timeout: EXEC_TIMEOUT_QUICK_MS });
124
116
  }
125
117
  catch (err) {
126
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
118
+ if ((process.env.PHREN_DEBUG))
127
119
  process.stderr.write(`[phren] setupSparseCheckout notAGitRepo: ${errorMessage(err)}\n`);
128
120
  return;
129
121
  }
@@ -184,10 +176,9 @@ function symlinkFile(src, dest, managedRoot) {
184
176
  if (stat.isSymbolicLink()) {
185
177
  const currentTarget = fs.readlinkSync(dest);
186
178
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
187
- const managedPrefix = path.resolve(managedRoot) + path.sep;
188
179
  if (resolvedTarget === path.resolve(src))
189
180
  return true;
190
- if (!resolvedTarget.startsWith(managedPrefix)) {
181
+ if (!isManagedSymlink(dest, managedRoot)) {
191
182
  log(` preserve existing symlink: ${dest}`);
192
183
  return false;
193
184
  }
@@ -238,8 +229,7 @@ function writeManagedAgentsFile(src, dest, content, managedRoot) {
238
229
  if (stat.isSymbolicLink()) {
239
230
  const currentTarget = fs.readlinkSync(dest);
240
231
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
241
- const managedPrefix = path.resolve(managedRoot) + path.sep;
242
- if (resolvedTarget === path.resolve(src) || resolvedTarget.startsWith(managedPrefix)) {
232
+ if (resolvedTarget === path.resolve(src) || isManagedSymlink(dest, managedRoot)) {
243
233
  fs.unlinkSync(dest);
244
234
  }
245
235
  else {
@@ -278,7 +268,7 @@ function linkGlobal(phrenPath, tools) {
278
268
  symlinkFile(globalClaude, path.join(copilotInstrDir, "copilot-instructions.md"), phrenPath);
279
269
  }
280
270
  catch (err) {
281
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
271
+ if ((process.env.PHREN_DEBUG))
282
272
  process.stderr.write(`[phren] linkGlobal copilotInstructions: ${errorMessage(err)}\n`);
283
273
  }
284
274
  }
@@ -324,7 +314,7 @@ function linkProject(phrenPath, project, tools) {
324
314
  symlinkFile(src, path.join(copilotDir, "copilot-instructions.md"), phrenPath);
325
315
  }
326
316
  catch (err) {
327
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
317
+ if ((process.env.PHREN_DEBUG))
328
318
  process.stderr.write(`[phren] linkProject copilotInstructions: ${errorMessage(err)}\n`);
329
319
  }
330
320
  }
@@ -348,7 +338,7 @@ function linkProject(phrenPath, project, tools) {
348
338
  addTokenAnnotation(claudeFile);
349
339
  }
350
340
  catch (err) {
351
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
341
+ if ((process.env.PHREN_DEBUG))
352
342
  process.stderr.write(`[phren] linkProject tokenAnnotation: ${errorMessage(err)}\n`);
353
343
  }
354
344
  }
@@ -365,7 +355,7 @@ function linkProject(phrenPath, project, tools) {
365
355
  excludeEntries.push("AGENTS.md");
366
356
  }
367
357
  catch (err) {
368
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
358
+ if ((process.env.PHREN_DEBUG))
369
359
  process.stderr.write(`[phren] linkProject agentsMd: ${errorMessage(err)}\n`);
370
360
  }
371
361
  }
@@ -501,56 +491,7 @@ export async function runLink(phrenPath, opts = {}) {
501
491
  log(` MCP mode: ${mcpEnabled ? "ON (recommended)" : "OFF (hooks-only fallback)"}`);
502
492
  log(` Hooks mode: ${hooksEnabled ? "ON (active)" : "OFF (disabled)"}`);
503
493
  maybeOfferStarterTemplateUpdate(phrenPath);
504
- let mcpStatus = "no_settings";
505
- try {
506
- mcpStatus = configureClaude(phrenPath, { mcpEnabled, hooksEnabled }) ?? "installed";
507
- }
508
- catch (err) {
509
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
510
- process.stderr.write(`[phren] link configureClaude: ${errorMessage(err)}\n`);
511
- }
512
- logMcpTargetStatus("Claude", mcpStatus);
513
- let vsStatus = "no_vscode";
514
- try {
515
- vsStatus = configureVSCode(phrenPath, { mcpEnabled }) ?? "no_vscode";
516
- }
517
- catch (err) {
518
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
519
- process.stderr.write(`[phren] link configureVSCode: ${errorMessage(err)}\n`);
520
- }
521
- logMcpTargetStatus("VS Code", vsStatus);
522
- let cursorStatus = "no_cursor";
523
- try {
524
- cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled }) ?? "no_cursor";
525
- }
526
- catch (err) {
527
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
528
- process.stderr.write(`[phren] link configureCursorMcp: ${errorMessage(err)}\n`);
529
- }
530
- logMcpTargetStatus("Cursor", cursorStatus);
531
- let copilotStatus = "no_copilot";
532
- try {
533
- copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled }) ?? "no_copilot";
534
- }
535
- catch (err) {
536
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
537
- process.stderr.write(`[phren] link configureCopilotMcp: ${errorMessage(err)}\n`);
538
- }
539
- logMcpTargetStatus("Copilot CLI", copilotStatus);
540
- let codexStatus = "no_codex";
541
- try {
542
- codexStatus = configureCodexMcp(phrenPath, { mcpEnabled }) ?? "no_codex";
543
- }
544
- catch (err) {
545
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
546
- process.stderr.write(`[phren] link configureCodexMcp: ${errorMessage(err)}\n`);
547
- }
548
- logMcpTargetStatus("Codex", codexStatus);
549
- const mcpStatusForContext = [mcpStatus, vsStatus, cursorStatus, copilotStatus, codexStatus].some((s) => s === "installed" || s === "already_configured")
550
- ? "installed"
551
- : [mcpStatus, vsStatus, cursorStatus, copilotStatus, codexStatus].some((s) => s === "disabled" || s === "already_disabled")
552
- ? "disabled"
553
- : mcpStatus;
494
+ const mcpStatusForContext = configureMcpTargets(phrenPath, { mcpEnabled, hooksEnabled });
554
495
  // Register hooks for Copilot CLI, Cursor, Codex
555
496
  if (hooksEnabled) {
556
497
  const hookedTools = configureAllHooks(phrenPath, { tools: detectedTools });
@@ -566,7 +507,7 @@ export async function runLink(phrenPath, opts = {}) {
566
507
  log(` phren.SKILL.md written (agentskills-compatible tools)`);
567
508
  }
568
509
  catch (err) {
569
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
510
+ if ((process.env.PHREN_DEBUG))
570
511
  process.stderr.write(`[phren] link writeSkillMd: ${errorMessage(err)}\n`);
571
512
  }
572
513
  log("");
@@ -1,17 +1,9 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
- import * as path from "path";
4
- import * as crypto from "crypto";
5
- import { homePath } from "./shared.js";
3
+ import { homePath, atomicWriteText } from "./shared.js";
6
4
  function phrenMachineFilePath() {
7
5
  return homePath(".phren", ".machine-id");
8
6
  }
9
- function atomicWriteText(filePath, content) {
10
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
12
- fs.writeFileSync(tmpPath, content);
13
- fs.renameSync(tmpPath, filePath);
14
- }
15
7
  export function machineFilePath() {
16
8
  return phrenMachineFilePath();
17
9
  }
@@ -1,27 +1,38 @@
1
1
  import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
- import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, } from "./shared-governance.js";
4
- import { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, } from "./proactivity.js";
5
- import { readGovernanceInstallPreferences, writeGovernanceInstallPreferences, } from "./init-preferences.js";
6
- import { FINDING_SENSITIVITY_CONFIG } from "./cli-config.js";
3
+ import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, mergeConfig, VALID_TASK_MODES, VALID_FINDING_SENSITIVITY, } from "./shared-governance.js";
4
+ import { PROACTIVITY_LEVELS, } from "./proactivity.js";
5
+ import { writeGovernanceInstallPreferences, } from "./init-preferences.js";
6
+ import { FINDING_SENSITIVITY_CONFIG, buildProactivitySnapshot, checkProjectInProfile } from "./cli-config.js";
7
+ import { readProjectConfig, updateProjectConfigOverrides, } from "./project-config.js";
8
+ import { isValidProjectName } from "./utils.js";
7
9
  // ── Helpers ─────────────────────────────────────────────────────────────────
8
10
  function proactivitySnapshot(phrenPath) {
9
- const prefs = readGovernanceInstallPreferences(phrenPath);
10
- return {
11
- configured: {
12
- proactivity: prefs.proactivity ?? null,
13
- proactivityFindings: prefs.proactivityFindings ?? null,
14
- proactivityTask: prefs.proactivityTask ?? null,
15
- },
16
- effective: {
17
- proactivity: getProactivityLevel(phrenPath),
18
- proactivityFindings: getProactivityLevelForFindings(phrenPath),
19
- proactivityTask: getProactivityLevelForTask(phrenPath),
20
- },
21
- };
11
+ const snap = buildProactivitySnapshot(phrenPath);
12
+ return { configured: snap.configured, effective: snap.effective };
22
13
  }
23
- const TASK_MODES = ["off", "manual", "suggest", "auto"];
24
- const FINDING_SENSITIVITY_LEVELS = ["minimal", "conservative", "balanced", "aggressive"];
14
+ function validateProject(project) {
15
+ if (!isValidProjectName(project))
16
+ return `Invalid project name: "${project}".`;
17
+ return null;
18
+ }
19
+ function checkProjectRegistered(phrenPath, project) {
20
+ const warning = checkProjectInProfile(phrenPath, project);
21
+ if (warning) {
22
+ return `Project '${project}' is not registered in your active profile. Config was written but won't take effect until you run 'phren add' to register the project.`;
23
+ }
24
+ return null;
25
+ }
26
+ function normalizeProjectOverrides(raw) {
27
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
28
+ }
29
+ function getProjectOverrides(phrenPath, project) {
30
+ return normalizeProjectOverrides(readProjectConfig(phrenPath, project).config);
31
+ }
32
+ function hasOwnOverride(overrides, key) {
33
+ return Object.prototype.hasOwnProperty.call(overrides, key);
34
+ }
35
+ const projectParam = z.string().optional().describe("Project name. When provided, writes to that project's phren.project.yaml instead of global .governance/.");
25
36
  // ── Registration ────────────────────────────────────────────────────────────
26
37
  export function register(server, ctx) {
27
38
  const { phrenPath } = ctx;
@@ -30,15 +41,86 @@ export function register(server, ctx) {
30
41
  title: "◆ phren · get config",
31
42
  description: "Read current configuration for one or all config domains: proactivity, taskMode, " +
32
43
  "findingSensitivity, retention (policy), workflow, access, index. " +
33
- "Returns both configured and effective values.",
44
+ "Returns both configured and effective values. When project is provided, returns " +
45
+ "the merged view with project overrides applied and _source annotations.",
34
46
  inputSchema: z.object({
35
47
  domain: z
36
48
  .enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "access", "index", "all"])
37
49
  .optional()
38
50
  .describe("Config domain to read. Defaults to 'all'."),
51
+ project: projectParam,
39
52
  }),
40
- }, async ({ domain }) => {
53
+ }, async ({ domain, project }) => {
41
54
  const d = domain ?? "all";
55
+ if (project) {
56
+ const err = validateProject(project);
57
+ if (err)
58
+ return mcpResponse({ ok: false, error: err });
59
+ const resolved = mergeConfig(phrenPath, project);
60
+ const projectOverrides = getProjectOverrides(phrenPath, project);
61
+ function src(key) {
62
+ return hasOwnOverride(projectOverrides, key) ? "project" : "global";
63
+ }
64
+ const result = {
65
+ _project: project,
66
+ _note: "Values marked _source=project override the global default.",
67
+ };
68
+ if (d === "all" || d === "findingSensitivity") {
69
+ const level = resolved.findingSensitivity;
70
+ result.findingSensitivity = {
71
+ level,
72
+ ...FINDING_SENSITIVITY_CONFIG[level],
73
+ _source: src("findingSensitivity"),
74
+ };
75
+ }
76
+ if (d === "all" || d === "taskMode") {
77
+ result.taskMode = { taskMode: resolved.taskMode, _source: src("taskMode") };
78
+ }
79
+ if (d === "all" || d === "retention") {
80
+ result.retention = {
81
+ ...resolved.retentionPolicy,
82
+ _source: hasOwnOverride(projectOverrides, "retentionPolicy") ? "project" : "global",
83
+ };
84
+ }
85
+ if (d === "all" || d === "workflow") {
86
+ result.workflow = {
87
+ ...resolved.workflowPolicy,
88
+ _source: hasOwnOverride(projectOverrides, "workflowPolicy") ? "project" : "global",
89
+ };
90
+ }
91
+ if (d === "all" || d === "proactivity") {
92
+ const globalSnapshot = proactivitySnapshot(phrenPath).effective;
93
+ const base = resolved.proactivity.base ?? globalSnapshot.proactivity;
94
+ const findings = resolved.proactivity.findings ?? resolved.proactivity.base ?? globalSnapshot.proactivityFindings;
95
+ const tasks = resolved.proactivity.tasks ?? resolved.proactivity.base ?? globalSnapshot.proactivityTask;
96
+ result.proactivity = {
97
+ base,
98
+ findings,
99
+ tasks,
100
+ _source: {
101
+ base: hasOwnOverride(projectOverrides, "proactivity") ? "project" : "global",
102
+ findings: hasOwnOverride(projectOverrides, "proactivityFindings")
103
+ ? "project"
104
+ : hasOwnOverride(projectOverrides, "proactivity")
105
+ ? "project"
106
+ : "global",
107
+ tasks: hasOwnOverride(projectOverrides, "proactivityTask")
108
+ ? "project"
109
+ : hasOwnOverride(projectOverrides, "proactivity")
110
+ ? "project"
111
+ : "global",
112
+ },
113
+ };
114
+ }
115
+ if (d === "all" || d === "index") {
116
+ result.index = getIndexPolicy(phrenPath);
117
+ }
118
+ return mcpResponse({
119
+ ok: true,
120
+ message: `Config for ${d === "all" ? "all domains" : d} (project: ${project}).`,
121
+ data: result,
122
+ });
123
+ }
42
124
  const result = {};
43
125
  if (d === "all" || d === "proactivity") {
44
126
  result.proactivity = proactivitySnapshot(phrenPath);
@@ -72,16 +154,36 @@ export function register(server, ctx) {
72
154
  server.registerTool("set_proactivity", {
73
155
  title: "◆ phren · set proactivity",
74
156
  description: "Set the proactivity level for auto-capture. Controls how aggressively phren " +
75
- "captures findings and tasks. Supports base level, findings-specific, and task-specific overrides.",
157
+ "captures findings and tasks. Supports base level, findings-specific, and task-specific overrides. " +
158
+ "When project is provided, writes to that project's phren.project.yaml.",
76
159
  inputSchema: z.object({
77
160
  level: z.enum(PROACTIVITY_LEVELS).describe("Proactivity level: high, medium, or low."),
78
161
  scope: z
79
162
  .enum(["base", "findings", "tasks"])
80
163
  .optional()
81
164
  .describe("Which proactivity to set. Defaults to 'base'."),
165
+ project: projectParam,
82
166
  }),
83
- }, async ({ level, scope }) => {
167
+ }, async ({ level, scope, project }) => {
84
168
  const s = scope ?? "base";
169
+ if (project) {
170
+ const err = validateProject(project);
171
+ if (err)
172
+ return mcpResponse({ ok: false, error: err });
173
+ const warning = checkProjectRegistered(phrenPath, project);
174
+ const key = s === "base" ? "proactivity" : s === "findings" ? "proactivityFindings" : "proactivityTask";
175
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
176
+ ...current,
177
+ [key]: level,
178
+ }));
179
+ return mcpResponse({
180
+ ok: true,
181
+ message: warning
182
+ ? `Proactivity ${s} set to ${level} for project "${project}". WARNING: ${warning}`
183
+ : `Proactivity ${s} set to ${level} for project "${project}".`,
184
+ data: { project, scope: s, level, ...(warning ? { warning } : {}) },
185
+ });
186
+ }
85
187
  const patch = {};
86
188
  if (s === "base")
87
189
  patch.proactivity = level;
@@ -100,11 +202,30 @@ export function register(server, ctx) {
100
202
  server.registerTool("set_task_mode", {
101
203
  title: "◆ phren · set task mode",
102
204
  description: "Set the task automation mode: off (no auto-tasks), manual (user creates), " +
103
- "suggest (phren suggests, user approves), auto (phren creates automatically).",
205
+ "suggest (phren suggests, user approves), auto (phren creates automatically). " +
206
+ "When project is provided, writes to that project's phren.project.yaml.",
104
207
  inputSchema: z.object({
105
- mode: z.enum(TASK_MODES).describe("Task mode: off, manual, suggest, or auto."),
208
+ mode: z.enum(VALID_TASK_MODES).describe("Task mode: off, manual, suggest, or auto."),
209
+ project: projectParam,
106
210
  }),
107
- }, async ({ mode }) => {
211
+ }, async ({ mode, project }) => {
212
+ if (project) {
213
+ const err = validateProject(project);
214
+ if (err)
215
+ return mcpResponse({ ok: false, error: err });
216
+ const warning = checkProjectRegistered(phrenPath, project);
217
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
218
+ ...current,
219
+ taskMode: mode,
220
+ }));
221
+ return mcpResponse({
222
+ ok: true,
223
+ message: warning
224
+ ? `Task mode set to ${mode} for project "${project}". WARNING: ${warning}`
225
+ : `Task mode set to ${mode} for project "${project}".`,
226
+ data: { project, taskMode: mode, ...(warning ? { warning } : {}) },
227
+ });
228
+ }
108
229
  const result = updateWorkflowPolicy(phrenPath, { taskMode: mode });
109
230
  if (!result.ok) {
110
231
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
@@ -120,11 +241,31 @@ export function register(server, ctx) {
120
241
  title: "◆ phren · set finding sensitivity",
121
242
  description: "Set the finding capture sensitivity level. Controls how many findings phren captures per session. " +
122
243
  "minimal: only explicit asks. conservative: decisions/pitfalls only. " +
123
- "balanced: non-obvious patterns. aggressive: capture everything.",
244
+ "balanced: non-obvious patterns. aggressive: capture everything. " +
245
+ "When project is provided, writes to that project's phren.project.yaml.",
124
246
  inputSchema: z.object({
125
- level: z.enum(FINDING_SENSITIVITY_LEVELS).describe("Sensitivity level."),
247
+ level: z.enum(VALID_FINDING_SENSITIVITY).describe("Sensitivity level."),
248
+ project: projectParam,
126
249
  }),
127
- }, async ({ level }) => {
250
+ }, async ({ level, project }) => {
251
+ if (project) {
252
+ const err = validateProject(project);
253
+ if (err)
254
+ return mcpResponse({ ok: false, error: err });
255
+ const warning = checkProjectRegistered(phrenPath, project);
256
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
257
+ ...current,
258
+ findingSensitivity: level,
259
+ }));
260
+ const config = FINDING_SENSITIVITY_CONFIG[level];
261
+ return mcpResponse({
262
+ ok: true,
263
+ message: warning
264
+ ? `Finding sensitivity set to ${level} for project "${project}". WARNING: ${warning}`
265
+ : `Finding sensitivity set to ${level} for project "${project}".`,
266
+ data: { project, level, ...config, ...(warning ? { warning } : {}) },
267
+ });
268
+ }
128
269
  const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: level });
129
270
  if (!result.ok) {
130
271
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
@@ -140,7 +281,8 @@ export function register(server, ctx) {
140
281
  server.registerTool("set_retention_policy", {
141
282
  title: "◆ phren · set retention policy",
142
283
  description: "Update memory retention policy: TTL, retention days, auto-accept threshold, " +
143
- "minimum injection confidence, and decay curve.",
284
+ "minimum injection confidence, and decay curve. " +
285
+ "When project is provided, writes to that project's phren.project.yaml.",
144
286
  inputSchema: z.object({
145
287
  ttlDays: z.number().int().min(1).optional().describe("Days before a finding is considered for expiry."),
146
288
  retentionDays: z.number().int().min(1).optional().describe("Hard retention limit in days."),
@@ -155,20 +297,49 @@ export function register(server, ctx) {
155
297
  })
156
298
  .optional()
157
299
  .describe("Decay multipliers at 30/60/90/120 day marks."),
300
+ project: projectParam,
158
301
  }),
159
- }, async ({ ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay }) => {
160
- const patch = {};
302
+ }, async ({ ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay, project }) => {
303
+ if (project) {
304
+ const err = validateProject(project);
305
+ if (err)
306
+ return mcpResponse({ ok: false, error: err });
307
+ const warning = checkProjectRegistered(phrenPath, project);
308
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
309
+ const existingRetention = current.retentionPolicy ?? {};
310
+ const retentionPatch = { ...existingRetention };
311
+ if (ttlDays !== undefined)
312
+ retentionPatch.ttlDays = ttlDays;
313
+ if (retentionDays !== undefined)
314
+ retentionPatch.retentionDays = retentionDays;
315
+ if (autoAcceptThreshold !== undefined)
316
+ retentionPatch.autoAcceptThreshold = autoAcceptThreshold;
317
+ if (minInjectConfidence !== undefined)
318
+ retentionPatch.minInjectConfidence = minInjectConfidence;
319
+ if (decay !== undefined)
320
+ retentionPatch.decay = { ...(existingRetention.decay ?? {}), ...decay };
321
+ return { ...current, retentionPolicy: retentionPatch };
322
+ });
323
+ return mcpResponse({
324
+ ok: true,
325
+ message: warning
326
+ ? `Retention policy updated for project "${project}". WARNING: ${warning}`
327
+ : `Retention policy updated for project "${project}".`,
328
+ data: { project, retentionPolicy: next.config?.retentionPolicy ?? {}, ...(warning ? { warning } : {}) },
329
+ });
330
+ }
331
+ const globalPatch = {};
161
332
  if (ttlDays !== undefined)
162
- patch.ttlDays = ttlDays;
333
+ globalPatch.ttlDays = ttlDays;
163
334
  if (retentionDays !== undefined)
164
- patch.retentionDays = retentionDays;
335
+ globalPatch.retentionDays = retentionDays;
165
336
  if (autoAcceptThreshold !== undefined)
166
- patch.autoAcceptThreshold = autoAcceptThreshold;
337
+ globalPatch.autoAcceptThreshold = autoAcceptThreshold;
167
338
  if (minInjectConfidence !== undefined)
168
- patch.minInjectConfidence = minInjectConfidence;
339
+ globalPatch.minInjectConfidence = minInjectConfidence;
169
340
  if (decay !== undefined)
170
- patch.decay = decay;
171
- const result = updateRetentionPolicy(phrenPath, patch);
341
+ globalPatch.decay = decay;
342
+ const result = updateRetentionPolicy(phrenPath, globalPatch);
172
343
  if (!result.ok) {
173
344
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
174
345
  }
@@ -182,18 +353,52 @@ export function register(server, ctx) {
182
353
  server.registerTool("set_workflow_policy", {
183
354
  title: "◆ phren · set workflow policy",
184
355
  description: "Update workflow policy: low-confidence threshold, " +
185
- "risky sections list, task mode, and finding sensitivity.",
356
+ "risky sections list, task mode, and finding sensitivity. " +
357
+ "When project is provided, writes to that project's phren.project.yaml.",
186
358
  inputSchema: z.object({
187
359
  lowConfidenceThreshold: z.number().min(0).max(1).optional()
188
360
  .describe("Confidence below which items are flagged as low-confidence."),
189
361
  riskySections: z.array(z.enum(["Review", "Stale", "Conflicts"])).optional()
190
362
  .describe("Which queue sections are considered risky."),
191
- taskMode: z.enum(TASK_MODES).optional()
363
+ taskMode: z.enum(VALID_TASK_MODES).optional()
192
364
  .describe("Task automation mode."),
193
- findingSensitivity: z.enum(FINDING_SENSITIVITY_LEVELS).optional()
365
+ findingSensitivity: z.enum(VALID_FINDING_SENSITIVITY).optional()
194
366
  .describe("Finding capture sensitivity."),
367
+ project: projectParam,
195
368
  }),
196
- }, async ({ lowConfidenceThreshold, riskySections, taskMode, findingSensitivity }) => {
369
+ }, async ({ lowConfidenceThreshold, riskySections, taskMode, findingSensitivity, project }) => {
370
+ if (project) {
371
+ const err = validateProject(project);
372
+ if (err)
373
+ return mcpResponse({ ok: false, error: err });
374
+ const warning = checkProjectRegistered(phrenPath, project);
375
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
376
+ const nextConfig = { ...current };
377
+ const shouldUpdateWorkflowPolicy = (lowConfidenceThreshold !== undefined
378
+ || riskySections !== undefined
379
+ || current.workflowPolicy !== undefined);
380
+ if (shouldUpdateWorkflowPolicy) {
381
+ const existingWorkflow = current.workflowPolicy ?? {};
382
+ nextConfig.workflowPolicy = {
383
+ ...existingWorkflow,
384
+ ...(lowConfidenceThreshold !== undefined ? { lowConfidenceThreshold } : {}),
385
+ ...(riskySections !== undefined ? { riskySections } : {}),
386
+ };
387
+ }
388
+ if (taskMode !== undefined)
389
+ nextConfig.taskMode = taskMode;
390
+ if (findingSensitivity !== undefined)
391
+ nextConfig.findingSensitivity = findingSensitivity;
392
+ return nextConfig;
393
+ });
394
+ return mcpResponse({
395
+ ok: true,
396
+ message: warning
397
+ ? `Workflow policy updated for project "${project}". WARNING: ${warning}`
398
+ : `Workflow policy updated for project "${project}".`,
399
+ data: { project, config: next.config ?? {}, ...(warning ? { warning } : {}) },
400
+ });
401
+ }
197
402
  const patch = {};
198
403
  if (lowConfidenceThreshold !== undefined)
199
404
  patch.lowConfidenceThreshold = lowConfidenceThreshold;
@@ -2,7 +2,7 @@ import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
- import { isValidProjectName } from "./utils.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } from "./data-access.js";
7
7
  import { debugLog, findProjectNameCaseInsensitive, normalizeProjectNameForCreate } from "./shared.js";
8
8
  const importPayloadSchema = z.object({
@@ -75,8 +75,8 @@ export function register(server, ctx) {
75
75
  decoded = JSON.parse(rawData);
76
76
  }
77
77
  catch (err) {
78
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
79
- process.stderr.write(`[phren] import_project jsonParse: ${err instanceof Error ? err.message : String(err)}\n`);
78
+ if ((process.env.PHREN_DEBUG))
79
+ process.stderr.write(`[phren] import_project jsonParse: ${errorMessage(err)}\n`);
80
80
  return mcpResponse({ ok: false, error: "Invalid JSON input." });
81
81
  }
82
82
  const parsedResult = importPayloadSchema.safeParse(decoded);
@@ -241,8 +241,8 @@ export function register(server, ctx) {
241
241
  }
242
242
  }
243
243
  catch (err) {
244
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
245
- process.stderr.write(`[phren] import_project backupRestore: ${err instanceof Error ? err.message : String(err)}\n`);
244
+ if ((process.env.PHREN_DEBUG))
245
+ process.stderr.write(`[phren] import_project backupRestore: ${errorMessage(err)}\n`);
246
246
  }
247
247
  }
248
248
  return mcpResponse({
@@ -261,8 +261,8 @@ export function register(server, ctx) {
261
261
  }
262
262
  }
263
263
  catch (err) {
264
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
265
- process.stderr.write(`[phren] import_project backupCleanup: ${err instanceof Error ? err.message : String(err)}\n`);
264
+ if ((process.env.PHREN_DEBUG))
265
+ process.stderr.write(`[phren] import_project backupCleanup: ${errorMessage(err)}\n`);
266
266
  }
267
267
  }
268
268
  return mcpResponse({
@@ -299,7 +299,7 @@ export function register(server, ctx) {
299
299
  }
300
300
  catch (err) {
301
301
  fs.renameSync(archiveDir, projectDir);
302
- return mcpResponse({ ok: false, error: `Index rebuild failed after archive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
302
+ return mcpResponse({ ok: false, error: `Index rebuild failed after archive rename, rolled back: ${errorMessage(err)}` });
303
303
  }
304
304
  return mcpResponse({
305
305
  ok: true,
@@ -322,7 +322,7 @@ export function register(server, ctx) {
322
322
  }
323
323
  catch (err) {
324
324
  fs.renameSync(projectDir, archiveDir);
325
- return mcpResponse({ ok: false, error: `Index rebuild failed after unarchive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
325
+ return mcpResponse({ ok: false, error: `Index rebuild failed after unarchive rename, rolled back: ${errorMessage(err)}` });
326
326
  }
327
327
  return mcpResponse({
328
328
  ok: true,