@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
@@ -1,17 +1,132 @@
1
1
  import { getPhrenPath, readRootManifest } from "./shared.js";
2
2
  import { installPreferencesFile } from "./phren-paths.js";
3
- import { getIndexPolicy, updateIndexPolicy, getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, } from "./shared-governance.js";
4
- import { listMachines as listMachinesStore, listProfiles as listProfilesStore } from "./data-access.js";
3
+ import { getIndexPolicy, updateIndexPolicy, getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, VALID_TASK_MODES, VALID_FINDING_SENSITIVITY, } from "./shared-governance.js";
4
+ import { listMachines as listMachinesStore, listProfiles as listProfilesStore, listProfiles } from "./data-access.js";
5
5
  import { setTelemetryEnabled, getTelemetrySummary, resetTelemetry } from "./telemetry.js";
6
6
  import { governanceInstallPreferencesFile, readInstallPreferences, readGovernanceInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences, } from "./init-preferences.js";
7
7
  import { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForTask, getProactivityLevelForFindings, } from "./proactivity.js";
8
- import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, } from "./project-config.js";
8
+ import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, updateProjectConfigOverrides, } from "./project-config.js";
9
9
  import { isValidProjectName, learnedSynonymsPath, learnSynonym, loadLearnedSynonyms, removeLearnedSynonym, } from "./utils.js";
10
+ // ── Shared helpers ────────────────────────────────────────────────────────────
11
+ export function parseProjectArg(args) {
12
+ const project = args.find((a) => a.startsWith("--project="))?.slice("--project=".length)
13
+ ?? (args.indexOf("--project") !== -1 ? args[args.indexOf("--project") + 1] : undefined);
14
+ const rest = args.filter((a, i) => a !== "--project" && !a.startsWith("--project=") && args[i - 1] !== "--project");
15
+ return { project, rest };
16
+ }
17
+ export function checkProjectInProfile(phrenPath, project) {
18
+ const profiles = listProfiles(phrenPath);
19
+ if (profiles.ok) {
20
+ const registered = profiles.data.some((entry) => entry.projects.includes(project));
21
+ if (!registered) {
22
+ return `Warning: Project '${project}' not found in active profile. Run 'phren add /path/to/${project}' first.\n Config was written to ${phrenPath}/${project}/phren.project.yaml but won't be used until the project is registered.`;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+ function warnIfUnregistered(phrenPath, project) {
28
+ const warning = checkProjectInProfile(phrenPath, project);
29
+ if (warning)
30
+ console.error(warning);
31
+ }
32
+ export function buildProactivitySnapshot(phrenPath) {
33
+ const prefs = readGovernanceInstallPreferences(phrenPath);
34
+ return {
35
+ path: governanceInstallPreferencesFile(phrenPath),
36
+ configured: {
37
+ proactivity: prefs.proactivity ?? null,
38
+ proactivityFindings: prefs.proactivityFindings ?? null,
39
+ proactivityTask: prefs.proactivityTask ?? null,
40
+ },
41
+ effective: {
42
+ proactivity: getProactivityLevel(phrenPath),
43
+ proactivityFindings: getProactivityLevelForFindings(phrenPath),
44
+ proactivityTask: getProactivityLevelForTask(phrenPath),
45
+ },
46
+ };
47
+ }
48
+ // ── Config show ───────────────────────────────────────────────────────────────
49
+ function formatConfigAsTable(label, rows) {
50
+ const maxKey = Math.max(...rows.map(([k]) => k.length));
51
+ console.log(`\n${label}`);
52
+ for (const [k, v] of rows) {
53
+ console.log(` ${k.padEnd(maxKey)} ${v}`);
54
+ }
55
+ }
56
+ export function handleConfigShow(args) {
57
+ const phrenPath = getPhrenPath();
58
+ const { project: projectArg } = parseProjectArg(args);
59
+ if (projectArg) {
60
+ if (!isValidProjectName(projectArg)) {
61
+ console.error(`Invalid project name: "${projectArg}"`);
62
+ process.exit(1);
63
+ }
64
+ const resolved = mergeConfig(phrenPath, projectArg);
65
+ const inProfile = checkProjectInProfile(phrenPath, projectArg);
66
+ console.log(`\nConfig for project: ${projectArg}${inProfile ? "" : " (not in active profile)"}`);
67
+ formatConfigAsTable("Finding capture", [
68
+ ["sensitivity", resolved.findingSensitivity],
69
+ ["proactivity", resolved.proactivity.base ?? "(global)"],
70
+ ["proactivity.findings", resolved.proactivity.findings ?? "(global)"],
71
+ ["proactivity.tasks", resolved.proactivity.tasks ?? "(global)"],
72
+ ]);
73
+ formatConfigAsTable("Task automation", [
74
+ ["taskMode", resolved.taskMode],
75
+ ]);
76
+ formatConfigAsTable("Retention policy", [
77
+ ["ttlDays", String(resolved.retentionPolicy.ttlDays)],
78
+ ["retentionDays", String(resolved.retentionPolicy.retentionDays)],
79
+ ["autoAcceptThreshold", String(resolved.retentionPolicy.autoAcceptThreshold)],
80
+ ["minInjectConfidence", String(resolved.retentionPolicy.minInjectConfidence)],
81
+ ["decay.d30", String(resolved.retentionPolicy.decay.d30)],
82
+ ["decay.d60", String(resolved.retentionPolicy.decay.d60)],
83
+ ["decay.d90", String(resolved.retentionPolicy.decay.d90)],
84
+ ["decay.d120", String(resolved.retentionPolicy.decay.d120)],
85
+ ]);
86
+ formatConfigAsTable("Workflow policy", [
87
+ ["lowConfidenceThreshold", String(resolved.workflowPolicy.lowConfidenceThreshold)],
88
+ ["riskySections", resolved.workflowPolicy.riskySections.join(", ")],
89
+ ]);
90
+ if (!inProfile) {
91
+ console.error(`\nRun 'phren add /path/to/${projectArg}' to register this project.`);
92
+ }
93
+ console.log("");
94
+ return;
95
+ }
96
+ // Global config — no project
97
+ const retention = getRetentionPolicy(phrenPath);
98
+ const workflow = getWorkflowPolicy(phrenPath);
99
+ console.log("\nGlobal config (applies to all projects unless overridden)");
100
+ formatConfigAsTable("Finding capture", [
101
+ ["findingSensitivity", workflow.findingSensitivity],
102
+ ]);
103
+ formatConfigAsTable("Task automation", [
104
+ ["taskMode", workflow.taskMode],
105
+ ]);
106
+ formatConfigAsTable("Retention policy", [
107
+ ["ttlDays", String(retention.ttlDays)],
108
+ ["retentionDays", String(retention.retentionDays)],
109
+ ["autoAcceptThreshold", String(retention.autoAcceptThreshold)],
110
+ ["minInjectConfidence", String(retention.minInjectConfidence)],
111
+ ["decay.d30", String(retention.decay.d30)],
112
+ ["decay.d60", String(retention.decay.d60)],
113
+ ["decay.d90", String(retention.decay.d90)],
114
+ ["decay.d120", String(retention.decay.d120)],
115
+ ]);
116
+ formatConfigAsTable("Workflow policy", [
117
+ ["lowConfidenceThreshold", String(workflow.lowConfidenceThreshold)],
118
+ ["riskySections", workflow.riskySections.join(", ")],
119
+ ]);
120
+ console.log(`\nUse '--project <name>' to see merged config for a specific project.`);
121
+ console.log("");
122
+ }
10
123
  // ── Config router ────────────────────────────────────────────────────────────
11
124
  export async function handleConfig(args) {
12
125
  const sub = args[0];
13
126
  const rest = args.slice(1);
14
127
  switch (sub) {
128
+ case "show":
129
+ return handleConfigShow(rest);
15
130
  case "policy":
16
131
  return handleRetentionPolicy(rest);
17
132
  case "workflow":
@@ -42,6 +157,7 @@ export async function handleConfig(args) {
42
157
  console.log(`phren config - manage settings and policies
43
158
 
44
159
  Subcommands:
160
+ phren config show [--project <name>] Full merged config summary (what's actually active)
45
161
  phren config policy [get|set ...] Memory retention, TTL, confidence, decay
46
162
  phren config workflow [get|set ...] Risky-memory thresholds, task automation mode
47
163
  phren config index [get|set ...] Indexer include/exclude globs
@@ -137,30 +253,29 @@ function handleConfigSynonyms(args) {
137
253
  printSynonymsUsage();
138
254
  process.exit(1);
139
255
  }
140
- function proactivityConfigSnapshot(phrenPath) {
141
- const prefs = readGovernanceInstallPreferences(phrenPath);
142
- return {
143
- path: governanceInstallPreferencesFile(phrenPath),
144
- configured: {
145
- proactivity: prefs.proactivity ?? null,
146
- proactivityFindings: prefs.proactivityFindings ?? null,
147
- proactivityTask: prefs.proactivityTask ?? null,
148
- },
149
- effective: {
150
- proactivity: getProactivityLevel(phrenPath),
151
- proactivityFindings: getProactivityLevelForFindings(phrenPath),
152
- proactivityTask: getProactivityLevelForTask(phrenPath),
153
- },
154
- };
155
- }
156
256
  function handleConfigProactivity(subcommand, args) {
157
257
  const phrenPath = getPhrenPath();
158
- const value = args[0];
258
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
259
+ const value = filteredArgs[0];
159
260
  if (value === undefined) {
160
- console.log(JSON.stringify(proactivityConfigSnapshot(phrenPath), null, 2));
261
+ if (projectArg) {
262
+ if (!isValidProjectName(projectArg)) {
263
+ console.error(`Invalid project name: "${projectArg}"`);
264
+ process.exit(1);
265
+ }
266
+ const resolved = mergeConfig(phrenPath, projectArg);
267
+ console.log(JSON.stringify({
268
+ _project: projectArg,
269
+ base: resolved.proactivity.base ?? null,
270
+ findings: resolved.proactivity.findings ?? null,
271
+ tasks: resolved.proactivity.tasks ?? null,
272
+ }, null, 2));
273
+ return;
274
+ }
275
+ console.log(JSON.stringify(buildProactivitySnapshot(phrenPath), null, 2));
161
276
  return;
162
277
  }
163
- if (args.length !== 1) {
278
+ if (filteredArgs.length !== 1) {
164
279
  printProactivityUsage(subcommand);
165
280
  process.exit(1);
166
281
  }
@@ -169,6 +284,25 @@ function handleConfigProactivity(subcommand, args) {
169
284
  printProactivityUsage(subcommand);
170
285
  process.exit(1);
171
286
  }
287
+ if (projectArg) {
288
+ if (!isValidProjectName(projectArg)) {
289
+ console.error(`Invalid project name: "${projectArg}"`);
290
+ process.exit(1);
291
+ }
292
+ warnIfUnregistered(phrenPath, projectArg);
293
+ const key = subcommand === "proactivity" ? "proactivity"
294
+ : subcommand === "proactivity.findings" ? "proactivityFindings"
295
+ : "proactivityTask";
296
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => ({ ...current, [key]: level }));
297
+ const resolved = mergeConfig(phrenPath, projectArg);
298
+ console.log(JSON.stringify({
299
+ _project: projectArg,
300
+ base: resolved.proactivity.base ?? null,
301
+ findings: resolved.proactivity.findings ?? null,
302
+ tasks: resolved.proactivity.tasks ?? null,
303
+ }, null, 2));
304
+ return;
305
+ }
172
306
  switch (subcommand) {
173
307
  case "proactivity":
174
308
  writeGovernanceInstallPreferences(phrenPath, { proactivity: level });
@@ -180,7 +314,7 @@ function handleConfigProactivity(subcommand, args) {
180
314
  writeGovernanceInstallPreferences(phrenPath, { proactivityTask: level });
181
315
  break;
182
316
  }
183
- console.log(JSON.stringify(proactivityConfigSnapshot(phrenPath), null, 2));
317
+ console.log(JSON.stringify(buildProactivitySnapshot(phrenPath), null, 2));
184
318
  }
185
319
  function projectOwnershipConfigSnapshot(phrenPath) {
186
320
  const prefs = readInstallPreferences(phrenPath);
@@ -213,61 +347,7 @@ function handleConfigProjectOwnership(args) {
213
347
  writeInstallPreferences(phrenPath, { projectOwnershipDefault: ownership });
214
348
  console.log(JSON.stringify(projectOwnershipConfigSnapshot(phrenPath), null, 2));
215
349
  }
216
- // ── Task mode ─────────────────────────────────────────────────────────────────
217
- const TASK_MODES = ["off", "manual", "suggest", "auto"];
218
- function normalizeTaskMode(raw) {
219
- if (!raw)
220
- return undefined;
221
- const normalized = raw.trim().toLowerCase();
222
- return TASK_MODES.includes(normalized) ? normalized : undefined;
223
- }
224
- function taskModeConfigSnapshot(phrenPath) {
225
- const policy = getWorkflowPolicy(phrenPath);
226
- return {
227
- taskMode: policy.taskMode,
228
- };
229
- }
230
- function handleConfigTaskMode(args) {
231
- const phrenPath = getPhrenPath();
232
- const action = args[0];
233
- if (!action || action === "get") {
234
- console.log(JSON.stringify(taskModeConfigSnapshot(phrenPath), null, 2));
235
- return;
236
- }
237
- if (action === "set") {
238
- const mode = normalizeTaskMode(args[1]);
239
- if (!mode) {
240
- console.error(`Usage: phren config task-mode set [${TASK_MODES.join("|")}]`);
241
- process.exit(1);
242
- }
243
- const result = updateWorkflowPolicy(phrenPath, { taskMode: mode });
244
- if (!result.ok) {
245
- console.error(result.error);
246
- if (result.code === "PERMISSION_DENIED")
247
- process.exit(1);
248
- return;
249
- }
250
- console.log(JSON.stringify(taskModeConfigSnapshot(phrenPath), null, 2));
251
- return;
252
- }
253
- // Bare value: phren config task-mode auto
254
- const mode = normalizeTaskMode(action);
255
- if (mode) {
256
- const result = updateWorkflowPolicy(phrenPath, { taskMode: mode });
257
- if (!result.ok) {
258
- console.error(result.error);
259
- if (result.code === "PERMISSION_DENIED")
260
- process.exit(1);
261
- return;
262
- }
263
- console.log(JSON.stringify(taskModeConfigSnapshot(phrenPath), null, 2));
264
- return;
265
- }
266
- console.error(`Usage: phren config task-mode [get|set <mode>|<mode>] — modes: ${TASK_MODES.join("|")}`);
267
- process.exit(1);
268
- }
269
- // ── Finding sensitivity ───────────────────────────────────────────────────────
270
- const FINDING_SENSITIVITY_LEVELS = ["minimal", "conservative", "balanced", "aggressive"];
350
+ // ── Finding sensitivity config ────────────────────────────────────────────────
271
351
  export const FINDING_SENSITIVITY_CONFIG = {
272
352
  minimal: {
273
353
  sessionCap: 0,
@@ -290,59 +370,94 @@ export const FINDING_SENSITIVITY_CONFIG = {
290
370
  agentInstruction: "Save everything worth remembering — err on the side of capturing.",
291
371
  },
292
372
  };
293
- function normalizeFindingSensitivity(v) {
294
- if (!v)
295
- return null;
296
- const lower = v.toLowerCase();
297
- if (FINDING_SENSITIVITY_LEVELS.includes(lower))
298
- return lower;
299
- return null;
300
- }
301
- function findingSensitivityConfigSnapshot(phrenPath) {
302
- const policy = getWorkflowPolicy(phrenPath);
303
- const level = policy.findingSensitivity;
304
- const config = FINDING_SENSITIVITY_CONFIG[level];
305
- return { level, ...config };
306
- }
307
- function handleConfigFindingSensitivity(args) {
373
+ function handleWorkflowField(args, opts) {
308
374
  const phrenPath = getPhrenPath();
309
- const action = args[0];
375
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
376
+ const action = filteredArgs[0];
310
377
  if (!action || action === "get") {
311
- console.log(JSON.stringify(findingSensitivityConfigSnapshot(phrenPath), null, 2));
312
- return;
313
- }
314
- if (action === "set") {
315
- const level = normalizeFindingSensitivity(args[1]);
316
- if (!level) {
317
- console.error(`Usage: phren config finding-sensitivity set [${FINDING_SENSITIVITY_LEVELS.join("|")}]`);
318
- process.exit(1);
319
- }
320
- const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: level });
321
- if (!result.ok) {
322
- console.error(result.error);
323
- if (result.code === "PERMISSION_DENIED")
378
+ if (projectArg) {
379
+ if (!isValidProjectName(projectArg)) {
380
+ console.error(`Invalid project name: "${projectArg}"`);
324
381
  process.exit(1);
382
+ }
383
+ const resolved = mergeConfig(phrenPath, projectArg);
384
+ const value = opts.getProjectValue(resolved);
385
+ console.log(JSON.stringify(opts.formatProjectOutput(projectArg, value), null, 2));
325
386
  return;
326
387
  }
327
- console.log(JSON.stringify(findingSensitivityConfigSnapshot(phrenPath), null, 2));
388
+ console.log(JSON.stringify(opts.getSnapshot(phrenPath), null, 2));
328
389
  return;
329
390
  }
330
- // Bare value: phren config finding-sensitivity balanced
331
- const level = normalizeFindingSensitivity(action);
332
- if (level) {
333
- const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: level });
391
+ const applyValue = (value) => {
392
+ if (projectArg) {
393
+ if (!isValidProjectName(projectArg)) {
394
+ console.error(`Invalid project name: "${projectArg}"`);
395
+ process.exit(1);
396
+ }
397
+ warnIfUnregistered(phrenPath, projectArg);
398
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => ({ ...current, [opts.projectOverrideKey]: value }));
399
+ const resolved = mergeConfig(phrenPath, projectArg);
400
+ const eff = opts.getProjectValue(resolved);
401
+ console.log(JSON.stringify(opts.formatProjectOutput(projectArg, eff), null, 2));
402
+ return;
403
+ }
404
+ const result = updateWorkflowPolicy(phrenPath, { [opts.workflowPatchKey]: value });
334
405
  if (!result.ok) {
335
406
  console.error(result.error);
336
407
  if (result.code === "PERMISSION_DENIED")
337
408
  process.exit(1);
338
409
  return;
339
410
  }
340
- console.log(JSON.stringify(findingSensitivityConfigSnapshot(phrenPath), null, 2));
341
- return;
411
+ console.log(JSON.stringify(opts.getSnapshot(phrenPath), null, 2));
412
+ };
413
+ if (action === "set") {
414
+ const value = opts.normalize(filteredArgs[1]);
415
+ if (!value) {
416
+ console.error(`Usage: phren config ${opts.fieldName} set [${opts.validValues.join("|")}]`);
417
+ process.exit(1);
418
+ }
419
+ return applyValue(value);
342
420
  }
343
- console.error(`Usage: phren config finding-sensitivity [get|set <level>|<level>] — levels: ${FINDING_SENSITIVITY_LEVELS.join("|")}`);
421
+ const value = opts.normalize(action);
422
+ if (value)
423
+ return applyValue(value);
424
+ console.error(`Usage: phren config ${opts.fieldName} [--project <name>] [get|set <value>|<value>] — values: ${opts.validValues.join("|")}`);
344
425
  process.exit(1);
345
426
  }
427
+ function normalizeFromList(raw, validValues) {
428
+ if (!raw)
429
+ return null;
430
+ const lower = raw.trim().toLowerCase();
431
+ return validValues.includes(lower) ? lower : null;
432
+ }
433
+ function handleConfigTaskMode(args) {
434
+ handleWorkflowField(args, {
435
+ fieldName: "task-mode",
436
+ validValues: VALID_TASK_MODES,
437
+ normalize: (raw) => normalizeFromList(raw, VALID_TASK_MODES),
438
+ getSnapshot: (phrenPath) => ({ taskMode: getWorkflowPolicy(phrenPath).taskMode }),
439
+ getProjectValue: (resolved) => resolved.taskMode,
440
+ formatProjectOutput: (proj, value) => ({ _project: proj, taskMode: value }),
441
+ workflowPatchKey: "taskMode",
442
+ projectOverrideKey: "taskMode",
443
+ });
444
+ }
445
+ function handleConfigFindingSensitivity(args) {
446
+ handleWorkflowField(args, {
447
+ fieldName: "finding-sensitivity",
448
+ validValues: VALID_FINDING_SENSITIVITY,
449
+ normalize: (raw) => normalizeFromList(raw, VALID_FINDING_SENSITIVITY),
450
+ getSnapshot: (phrenPath) => {
451
+ const policy = getWorkflowPolicy(phrenPath);
452
+ const level = policy.findingSensitivity;
453
+ return { level, ...FINDING_SENSITIVITY_CONFIG[level] };
454
+ },
455
+ getProjectValue: (resolved) => resolved.findingSensitivity,
456
+ formatProjectOutput: (proj, value) => ({ _project: proj, level: value, ...FINDING_SENSITIVITY_CONFIG[value] }),
457
+ workflowPatchKey: "findingSensitivity",
458
+ projectOverrideKey: "findingSensitivity",
459
+ });
460
+ }
346
461
  // ── LLM config ───────────────────────────────────────────────────────────────
347
462
  const EXPENSIVE_MODEL_RE = /opus|sonnet|gpt-4(?!o-mini)/i;
348
463
  const DEFAULT_LLM_MODEL = "gpt-4o-mini / claude-haiku-4-5-20251001";
@@ -429,7 +544,7 @@ export async function handleIndexPolicy(args) {
429
544
  }
430
545
  const result = updateIndexPolicy(getPhrenPath(), patch);
431
546
  if (!result.ok) {
432
- console.log(result.error);
547
+ console.error(result.error);
433
548
  if (result.code === "PERMISSION_DENIED")
434
549
  process.exit(1);
435
550
  return;
@@ -442,10 +557,20 @@ export async function handleIndexPolicy(args) {
442
557
  }
443
558
  // ── Memory policy ────────────────────────────────────────────────────────────
444
559
  export async function handleRetentionPolicy(args) {
445
- if (!args.length || args[0] === "get") {
446
- console.log(JSON.stringify(getRetentionPolicy(getPhrenPath()), null, 2));
447
- const dedupOn = (process.env.PHREN_FEATURE_SEMANTIC_DEDUP ?? (process.env.PHREN_FEATURE_SEMANTIC_DEDUP || process.env.PHREN_FEATURE_SEMANTIC_DEDUP)) === "1";
448
- const conflictOn = (process.env.PHREN_FEATURE_SEMANTIC_CONFLICT ?? (process.env.PHREN_FEATURE_SEMANTIC_CONFLICT || process.env.PHREN_FEATURE_SEMANTIC_CONFLICT)) === "1";
560
+ const phrenPath = getPhrenPath();
561
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
562
+ if (!filteredArgs.length || filteredArgs[0] === "get") {
563
+ if (projectArg) {
564
+ if (!isValidProjectName(projectArg)) {
565
+ console.error(`Invalid project name: "${projectArg}"`);
566
+ process.exit(1);
567
+ }
568
+ const resolved = mergeConfig(phrenPath, projectArg);
569
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.retentionPolicy }, null, 2));
570
+ return;
571
+ }
572
+ console.log(JSON.stringify(getRetentionPolicy(phrenPath), null, 2));
573
+ const conflictOn = process.env.PHREN_FEATURE_SEMANTIC_CONFLICT === "1";
449
574
  process.stderr.write(`\nDedup: free Jaccard similarity scan on every add_finding (no API key needed).\n`);
450
575
  process.stderr.write(` Near-matches (30–55% overlap) are returned in the response for the agent to decide.\n`);
451
576
  if (conflictOn) {
@@ -458,9 +583,40 @@ export async function handleRetentionPolicy(args) {
458
583
  }
459
584
  return;
460
585
  }
461
- if (args[0] === "set") {
586
+ if (filteredArgs[0] === "set") {
587
+ if (projectArg) {
588
+ if (!isValidProjectName(projectArg)) {
589
+ console.error(`Invalid project name: "${projectArg}"`);
590
+ process.exit(1);
591
+ }
592
+ warnIfUnregistered(phrenPath, projectArg);
593
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => {
594
+ const existingRetention = current.retentionPolicy ?? {};
595
+ const retentionPatch = { ...existingRetention };
596
+ for (const arg of filteredArgs.slice(1)) {
597
+ if (!arg.startsWith("--"))
598
+ continue;
599
+ const [k, v] = arg.slice(2).split("=");
600
+ if (!k || v === undefined)
601
+ continue;
602
+ const num = Number(v);
603
+ const value = Number.isNaN(num) ? v : num;
604
+ if (k.startsWith("decay.")) {
605
+ retentionPatch.decay = { ...(retentionPatch.decay ?? {}) };
606
+ retentionPatch.decay[k.slice("decay.".length)] = value;
607
+ }
608
+ else {
609
+ retentionPatch[k] = value;
610
+ }
611
+ }
612
+ return { ...current, retentionPolicy: retentionPatch };
613
+ });
614
+ const resolved = mergeConfig(phrenPath, projectArg);
615
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.retentionPolicy }, null, 2));
616
+ return;
617
+ }
462
618
  const patch = {};
463
- for (const arg of args.slice(1)) {
619
+ for (const arg of filteredArgs.slice(1)) {
464
620
  if (!arg.startsWith("--"))
465
621
  continue;
466
622
  const [k, v] = arg.slice(2).split("=");
@@ -476,9 +632,9 @@ export async function handleRetentionPolicy(args) {
476
632
  patch[k] = value;
477
633
  }
478
634
  }
479
- const result = updateRetentionPolicy(getPhrenPath(), patch);
635
+ const result = updateRetentionPolicy(phrenPath, patch);
480
636
  if (!result.ok) {
481
- console.log(result.error);
637
+ console.error(result.error);
482
638
  if (result.code === "PERMISSION_DENIED")
483
639
  process.exit(1);
484
640
  return;
@@ -486,18 +642,68 @@ export async function handleRetentionPolicy(args) {
486
642
  console.log(JSON.stringify(result.data, null, 2));
487
643
  return;
488
644
  }
489
- console.error("Usage: phren config policy [get|set --ttlDays=120 --retentionDays=365 --autoAcceptThreshold=0.75 --minInjectConfidence=0.35 --decay.d30=1 --decay.d60=0.85 --decay.d90=0.65 --decay.d120=0.45]");
645
+ console.error("Usage: phren config policy [--project <name>] [get|set --ttlDays=120 --retentionDays=365 --autoAcceptThreshold=0.75 --minInjectConfidence=0.35 --decay.d30=1 --decay.d60=0.85 --decay.d90=0.65 --decay.d120=0.45]");
490
646
  process.exit(1);
491
647
  }
492
648
  // ── Memory workflow ──────────────────────────────────────────────────────────
493
649
  export async function handleWorkflowPolicy(args) {
494
- if (!args.length || args[0] === "get") {
495
- console.log(JSON.stringify(getWorkflowPolicy(getPhrenPath()), null, 2));
650
+ const phrenPath = getPhrenPath();
651
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
652
+ if (!filteredArgs.length || filteredArgs[0] === "get") {
653
+ if (projectArg) {
654
+ if (!isValidProjectName(projectArg)) {
655
+ console.error(`Invalid project name: "${projectArg}"`);
656
+ process.exit(1);
657
+ }
658
+ const resolved = mergeConfig(phrenPath, projectArg);
659
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.workflowPolicy }, null, 2));
660
+ return;
661
+ }
662
+ console.log(JSON.stringify(getWorkflowPolicy(phrenPath), null, 2));
496
663
  return;
497
664
  }
498
- if (args[0] === "set") {
665
+ if (filteredArgs[0] === "set") {
666
+ if (projectArg) {
667
+ if (!isValidProjectName(projectArg)) {
668
+ console.error(`Invalid project name: "${projectArg}"`);
669
+ process.exit(1);
670
+ }
671
+ warnIfUnregistered(phrenPath, projectArg);
672
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => {
673
+ const nextConfig = { ...current };
674
+ const existingWorkflow = current.workflowPolicy ?? {};
675
+ const workflowPatch = { ...existingWorkflow };
676
+ for (const arg of filteredArgs.slice(1)) {
677
+ if (!arg.startsWith("--"))
678
+ continue;
679
+ const [k, v] = arg.slice(2).split("=");
680
+ if (!k || v === undefined)
681
+ continue;
682
+ if (k === "riskySections") {
683
+ workflowPatch.riskySections = v.split(",").map((s) => s.trim()).filter(Boolean);
684
+ }
685
+ else if (k === "taskMode") {
686
+ nextConfig.taskMode = v;
687
+ }
688
+ else if (k === "findingSensitivity") {
689
+ nextConfig.findingSensitivity = v;
690
+ }
691
+ else {
692
+ const num = Number(v);
693
+ workflowPatch[k] = Number.isNaN(num) ? v : num;
694
+ }
695
+ }
696
+ if (Object.keys(workflowPatch).length > 0 || existingWorkflow !== current.workflowPolicy) {
697
+ nextConfig.workflowPolicy = workflowPatch;
698
+ }
699
+ return nextConfig;
700
+ });
701
+ const resolved = mergeConfig(phrenPath, projectArg);
702
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.workflowPolicy }, null, 2));
703
+ return;
704
+ }
499
705
  const patch = {};
500
- for (const arg of args.slice(1)) {
706
+ for (const arg of filteredArgs.slice(1)) {
501
707
  if (!arg.startsWith("--"))
502
708
  continue;
503
709
  const [k, v] = arg.slice(2).split("=");
@@ -511,7 +717,7 @@ export async function handleWorkflowPolicy(args) {
511
717
  patch[k] = Number.isNaN(num) ? v : num;
512
718
  }
513
719
  }
514
- const result = updateWorkflowPolicy(getPhrenPath(), patch);
720
+ const result = updateWorkflowPolicy(phrenPath, patch);
515
721
  if (!result.ok) {
516
722
  console.log(result.error);
517
723
  if (result.code === "PERMISSION_DENIED")
@@ -521,7 +727,7 @@ export async function handleWorkflowPolicy(args) {
521
727
  console.log(JSON.stringify(result.data, null, 2));
522
728
  return;
523
729
  }
524
- console.error("Usage: phren config workflow [get|set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts --taskMode=manual]");
730
+ console.error("Usage: phren config workflow [--project <name>] [get|set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts --taskMode=manual]");
525
731
  process.exit(1);
526
732
  }
527
733
  // ── Machines and profiles ────────────────────────────────────────────────────
@@ -240,9 +240,10 @@ export async function handleExtractMemories(projectArg, cwdArg, silent = false,
240
240
  console.log(`Skipped memory extraction for ${project}: findings proactivity is low.`);
241
241
  return;
242
242
  }
243
- const days = Number.parseInt((process.env.PHREN_MEMORY_EXTRACT_WINDOW_DAYS) || "30", 10);
243
+ const rawDays = Number.parseInt((process.env.PHREN_MEMORY_EXTRACT_WINDOW_DAYS) || "30", 10);
244
+ const days = Number.isNaN(rawDays) ? 30 : Math.max(1, rawDays);
244
245
  const threshold = Number.parseFloat((process.env.PHREN_MEMORY_AUTO_ACCEPT) || String(getRetentionPolicy(getPhrenPath()).autoAcceptThreshold));
245
- const records = parseGitLogRecords(repoRoot, Number.isNaN(days) ? 30 : days);
246
+ const records = parseGitLogRecords(repoRoot, days);
246
247
  const ghCandidates = isFeatureEnabled("PHREN_FEATURE_GH_MINING", false)
247
248
  ? await mineGithubCandidates(repoRoot)
248
249
  : [];