@phren/cli 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +2 -8
  2. package/mcp/dist/cli-actions.js +5 -5
  3. package/mcp/dist/cli-config.js +334 -127
  4. package/mcp/dist/cli-govern.js +35 -63
  5. package/mcp/dist/cli-graph.js +3 -2
  6. package/mcp/dist/cli-hooks-globs.js +2 -1
  7. package/mcp/dist/cli-hooks-output.js +3 -3
  8. package/mcp/dist/cli-hooks.js +39 -32
  9. package/mcp/dist/cli-namespaces.js +15 -5
  10. package/mcp/dist/cli-search.js +2 -2
  11. package/mcp/dist/content-archive.js +2 -2
  12. package/mcp/dist/content-dedup.js +9 -9
  13. package/mcp/dist/embedding.js +7 -7
  14. package/mcp/dist/entrypoint.js +129 -102
  15. package/mcp/dist/governance-locks.js +6 -5
  16. package/mcp/dist/governance-policy.js +155 -2
  17. package/mcp/dist/governance-scores.js +3 -3
  18. package/mcp/dist/hooks.js +39 -18
  19. package/mcp/dist/index.js +4 -4
  20. package/mcp/dist/init-config.js +3 -24
  21. package/mcp/dist/init-setup.js +5 -5
  22. package/mcp/dist/init.js +170 -23
  23. package/mcp/dist/link-checksums.js +3 -2
  24. package/mcp/dist/link-context.js +1 -1
  25. package/mcp/dist/link-doctor.js +3 -3
  26. package/mcp/dist/link-skills.js +98 -12
  27. package/mcp/dist/link.js +17 -27
  28. package/mcp/dist/machine-identity.js +1 -9
  29. package/mcp/dist/mcp-config.js +247 -42
  30. package/mcp/dist/mcp-data.js +9 -9
  31. package/mcp/dist/mcp-extract-facts.js +1 -1
  32. package/mcp/dist/mcp-extract.js +2 -2
  33. package/mcp/dist/mcp-finding.js +6 -6
  34. package/mcp/dist/mcp-graph.js +11 -11
  35. package/mcp/dist/mcp-ops.js +18 -18
  36. package/mcp/dist/mcp-search.js +8 -8
  37. package/mcp/dist/memory-ui-page.js +23 -0
  38. package/mcp/dist/memory-ui-scripts.js +210 -27
  39. package/mcp/dist/memory-ui-server.js +115 -3
  40. package/mcp/dist/phren-paths.js +7 -7
  41. package/mcp/dist/profile-store.js +2 -2
  42. package/mcp/dist/project-config.js +63 -16
  43. package/mcp/dist/session-utils.js +3 -2
  44. package/mcp/dist/shared-fragment-graph.js +22 -21
  45. package/mcp/dist/shared-index.js +144 -105
  46. package/mcp/dist/shared-retrieval.js +19 -13
  47. package/mcp/dist/shared-search-fallback.js +13 -13
  48. package/mcp/dist/shared-sqljs.js +3 -2
  49. package/mcp/dist/shared.js +3 -3
  50. package/mcp/dist/shell-input.js +1 -1
  51. package/mcp/dist/shell-state-store.js +1 -1
  52. package/mcp/dist/shell-view.js +3 -2
  53. package/mcp/dist/shell.js +1 -1
  54. package/mcp/dist/skill-files.js +4 -10
  55. package/mcp/dist/skill-registry.js +3 -0
  56. package/mcp/dist/status.js +41 -13
  57. package/mcp/dist/task-hygiene.js +1 -1
  58. package/mcp/dist/telemetry.js +5 -4
  59. package/mcp/dist/update.js +1 -1
  60. package/mcp/dist/utils.js +3 -3
  61. package/package.json +2 -2
  62. package/starter/global/skills/audit.md +106 -0
  63. package/mcp/dist/shared-paths.js +0 -1
package/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  <p align="center">
6
6
  <a href="https://www.npmjs.com/package/@phren/cli"><img src="https://img.shields.io/npm/v/%40phren%2Fcli?style=flat&labelColor=0D0D0D&color=7C3AED" alt="npm version"></a>
7
7
  <a href="https://github.com/alaarab/phren/blob/main/LICENSE"><img src="https://img.shields.io/github/license/alaarab/phren?style=flat&labelColor=0D0D0D&color=7C3AED" alt="license"></a>
8
+ <a href="https://alaarab.github.io/phren/"><img src="https://img.shields.io/badge/docs-alaarab.github.io%2Fphren-7C3AED?style=flat&labelColor=0D0D0D" alt="docs"></a>
9
+ <a href="https://alaarab.github.io/phren/whitepaper.pdf"><img src="https://img.shields.io/badge/whitepaper-PDF-7C3AED?style=flat&labelColor=0D0D0D" alt="whitepaper"></a>
8
10
  </p>
9
11
 
10
12
  <p align="center">
@@ -47,14 +49,6 @@ Init detects your tools, registers MCP servers, and installs lifecycle hooks. Af
47
49
 
48
50
  To add a project later, run `phren add` from that directory. To browse what phren knows, run `phren` to open the interactive shell.
49
51
 
50
- ## Learn more
51
-
52
- - [Documentation site](https://alaarab.github.io/phren/)
53
- - [Whitepaper (PDF)](https://alaarab.github.io/phren/whitepaper.pdf)
54
- - [Architecture](docs/architecture.md) — how retrieval, governance, and persistence fit together
55
- - [Contributing](CONTRIBUTING.md) — how to add tools, skills, and tests
56
- - [Security](SECURITY.md)
57
-
58
52
  ---
59
53
 
60
54
  MIT License. Made by [Ala Arab](https://github.com/alaarab).
@@ -58,7 +58,7 @@ export async function handleAddFinding(project, learning) {
58
58
  console.log(result.message);
59
59
  }
60
60
  catch (err) {
61
- console.error(err instanceof Error ? err.message : String(err));
61
+ console.error(errorMessage(err));
62
62
  process.exit(1);
63
63
  }
64
64
  }
@@ -164,7 +164,7 @@ export async function handleDoctor(args) {
164
164
  }
165
165
  }
166
166
  catch (err) {
167
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
167
+ if ((process.env.PHREN_DEBUG))
168
168
  process.stderr.write(`[phren] doctor searchMissParse: ${errorMessage(err)}\n`);
169
169
  }
170
170
  }
@@ -181,7 +181,7 @@ export async function handleDoctor(args) {
181
181
  }
182
182
  }
183
183
  catch (err) {
184
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
184
+ if ((process.env.PHREN_DEBUG))
185
185
  process.stderr.write(`[phren] doctor searchMissAnalysis: ${errorMessage(err)}\n`);
186
186
  }
187
187
  try {
@@ -215,7 +215,7 @@ export async function handleDoctor(args) {
215
215
  }
216
216
  }
217
217
  catch (err) {
218
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
218
+ if ((process.env.PHREN_DEBUG))
219
219
  process.stderr.write(`[phren] doctor ollamaStatus: ${errorMessage(err)}\n`);
220
220
  }
221
221
  process.exit(result.ok ? 0 : 1);
@@ -257,7 +257,7 @@ export async function handleStatus() {
257
257
  console.log(`semantic-search: ${model} ready, ${formatEmbeddingCoverage(coverage)}`);
258
258
  }
259
259
  catch (err) {
260
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
260
+ if ((process.env.PHREN_DEBUG))
261
261
  process.stderr.write(`[phren] handleStatus semanticSearch: ${errorMessage(err)}\n`);
262
262
  }
263
263
  }
@@ -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,30 @@ 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
- }
256
+ const proactivityConfigSnapshot = buildProactivitySnapshot;
156
257
  function handleConfigProactivity(subcommand, args) {
157
258
  const phrenPath = getPhrenPath();
158
- const value = args[0];
259
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
260
+ const value = filteredArgs[0];
159
261
  if (value === undefined) {
262
+ if (projectArg) {
263
+ if (!isValidProjectName(projectArg)) {
264
+ console.error(`Invalid project name: "${projectArg}"`);
265
+ process.exit(1);
266
+ }
267
+ const resolved = mergeConfig(phrenPath, projectArg);
268
+ console.log(JSON.stringify({
269
+ _project: projectArg,
270
+ base: resolved.proactivity.base ?? null,
271
+ findings: resolved.proactivity.findings ?? null,
272
+ tasks: resolved.proactivity.tasks ?? null,
273
+ }, null, 2));
274
+ return;
275
+ }
160
276
  console.log(JSON.stringify(proactivityConfigSnapshot(phrenPath), null, 2));
161
277
  return;
162
278
  }
163
- if (args.length !== 1) {
279
+ if (filteredArgs.length !== 1) {
164
280
  printProactivityUsage(subcommand);
165
281
  process.exit(1);
166
282
  }
@@ -169,6 +285,25 @@ function handleConfigProactivity(subcommand, args) {
169
285
  printProactivityUsage(subcommand);
170
286
  process.exit(1);
171
287
  }
288
+ if (projectArg) {
289
+ if (!isValidProjectName(projectArg)) {
290
+ console.error(`Invalid project name: "${projectArg}"`);
291
+ process.exit(1);
292
+ }
293
+ warnIfUnregistered(phrenPath, projectArg);
294
+ const key = subcommand === "proactivity" ? "proactivity"
295
+ : subcommand === "proactivity.findings" ? "proactivityFindings"
296
+ : "proactivityTask";
297
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => ({ ...current, [key]: level }));
298
+ const resolved = mergeConfig(phrenPath, projectArg);
299
+ console.log(JSON.stringify({
300
+ _project: projectArg,
301
+ base: resolved.proactivity.base ?? null,
302
+ findings: resolved.proactivity.findings ?? null,
303
+ tasks: resolved.proactivity.tasks ?? null,
304
+ }, null, 2));
305
+ return;
306
+ }
172
307
  switch (subcommand) {
173
308
  case "proactivity":
174
309
  writeGovernanceInstallPreferences(phrenPath, { proactivity: level });
@@ -213,61 +348,7 @@ function handleConfigProjectOwnership(args) {
213
348
  writeInstallPreferences(phrenPath, { projectOwnershipDefault: ownership });
214
349
  console.log(JSON.stringify(projectOwnershipConfigSnapshot(phrenPath), null, 2));
215
350
  }
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"];
351
+ // ── Finding sensitivity config ────────────────────────────────────────────────
271
352
  export const FINDING_SENSITIVITY_CONFIG = {
272
353
  minimal: {
273
354
  sessionCap: 0,
@@ -290,59 +371,94 @@ export const FINDING_SENSITIVITY_CONFIG = {
290
371
  agentInstruction: "Save everything worth remembering — err on the side of capturing.",
291
372
  },
292
373
  };
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) {
374
+ function handleWorkflowField(args, opts) {
308
375
  const phrenPath = getPhrenPath();
309
- const action = args[0];
376
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
377
+ const action = filteredArgs[0];
310
378
  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")
379
+ if (projectArg) {
380
+ if (!isValidProjectName(projectArg)) {
381
+ console.error(`Invalid project name: "${projectArg}"`);
324
382
  process.exit(1);
383
+ }
384
+ const resolved = mergeConfig(phrenPath, projectArg);
385
+ const value = opts.getProjectValue(resolved);
386
+ console.log(JSON.stringify(opts.formatProjectOutput(projectArg, value), null, 2));
325
387
  return;
326
388
  }
327
- console.log(JSON.stringify(findingSensitivityConfigSnapshot(phrenPath), null, 2));
389
+ console.log(JSON.stringify(opts.getSnapshot(phrenPath), null, 2));
328
390
  return;
329
391
  }
330
- // Bare value: phren config finding-sensitivity balanced
331
- const level = normalizeFindingSensitivity(action);
332
- if (level) {
333
- const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: level });
392
+ const applyValue = (value) => {
393
+ if (projectArg) {
394
+ if (!isValidProjectName(projectArg)) {
395
+ console.error(`Invalid project name: "${projectArg}"`);
396
+ process.exit(1);
397
+ }
398
+ warnIfUnregistered(phrenPath, projectArg);
399
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => ({ ...current, [opts.projectOverrideKey]: value }));
400
+ const resolved = mergeConfig(phrenPath, projectArg);
401
+ const eff = opts.getProjectValue(resolved);
402
+ console.log(JSON.stringify(opts.formatProjectOutput(projectArg, eff), null, 2));
403
+ return;
404
+ }
405
+ const result = updateWorkflowPolicy(phrenPath, { [opts.workflowPatchKey]: value });
334
406
  if (!result.ok) {
335
407
  console.error(result.error);
336
408
  if (result.code === "PERMISSION_DENIED")
337
409
  process.exit(1);
338
410
  return;
339
411
  }
340
- console.log(JSON.stringify(findingSensitivityConfigSnapshot(phrenPath), null, 2));
341
- return;
412
+ console.log(JSON.stringify(opts.getSnapshot(phrenPath), null, 2));
413
+ };
414
+ if (action === "set") {
415
+ const value = opts.normalize(filteredArgs[1]);
416
+ if (!value) {
417
+ console.error(`Usage: phren config ${opts.fieldName} set [${opts.validValues.join("|")}]`);
418
+ process.exit(1);
419
+ }
420
+ return applyValue(value);
342
421
  }
343
- console.error(`Usage: phren config finding-sensitivity [get|set <level>|<level>] — levels: ${FINDING_SENSITIVITY_LEVELS.join("|")}`);
422
+ const value = opts.normalize(action);
423
+ if (value)
424
+ return applyValue(value);
425
+ console.error(`Usage: phren config ${opts.fieldName} [--project <name>] [get|set <value>|<value>] — values: ${opts.validValues.join("|")}`);
344
426
  process.exit(1);
345
427
  }
428
+ function normalizeFromList(raw, validValues) {
429
+ if (!raw)
430
+ return null;
431
+ const lower = raw.trim().toLowerCase();
432
+ return validValues.includes(lower) ? lower : null;
433
+ }
434
+ function handleConfigTaskMode(args) {
435
+ handleWorkflowField(args, {
436
+ fieldName: "task-mode",
437
+ validValues: VALID_TASK_MODES,
438
+ normalize: (raw) => normalizeFromList(raw, VALID_TASK_MODES),
439
+ getSnapshot: (phrenPath) => ({ taskMode: getWorkflowPolicy(phrenPath).taskMode }),
440
+ getProjectValue: (resolved) => resolved.taskMode,
441
+ formatProjectOutput: (proj, value) => ({ _project: proj, taskMode: value }),
442
+ workflowPatchKey: "taskMode",
443
+ projectOverrideKey: "taskMode",
444
+ });
445
+ }
446
+ function handleConfigFindingSensitivity(args) {
447
+ handleWorkflowField(args, {
448
+ fieldName: "finding-sensitivity",
449
+ validValues: VALID_FINDING_SENSITIVITY,
450
+ normalize: (raw) => normalizeFromList(raw, VALID_FINDING_SENSITIVITY),
451
+ getSnapshot: (phrenPath) => {
452
+ const policy = getWorkflowPolicy(phrenPath);
453
+ const level = policy.findingSensitivity;
454
+ return { level, ...FINDING_SENSITIVITY_CONFIG[level] };
455
+ },
456
+ getProjectValue: (resolved) => resolved.findingSensitivity,
457
+ formatProjectOutput: (proj, value) => ({ _project: proj, level: value, ...FINDING_SENSITIVITY_CONFIG[value] }),
458
+ workflowPatchKey: "findingSensitivity",
459
+ projectOverrideKey: "findingSensitivity",
460
+ });
461
+ }
346
462
  // ── LLM config ───────────────────────────────────────────────────────────────
347
463
  const EXPENSIVE_MODEL_RE = /opus|sonnet|gpt-4(?!o-mini)/i;
348
464
  const DEFAULT_LLM_MODEL = "gpt-4o-mini / claude-haiku-4-5-20251001";
@@ -442,10 +558,20 @@ export async function handleIndexPolicy(args) {
442
558
  }
443
559
  // ── Memory policy ────────────────────────────────────────────────────────────
444
560
  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";
561
+ const phrenPath = getPhrenPath();
562
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
563
+ if (!filteredArgs.length || filteredArgs[0] === "get") {
564
+ if (projectArg) {
565
+ if (!isValidProjectName(projectArg)) {
566
+ console.error(`Invalid project name: "${projectArg}"`);
567
+ process.exit(1);
568
+ }
569
+ const resolved = mergeConfig(phrenPath, projectArg);
570
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.retentionPolicy }, null, 2));
571
+ return;
572
+ }
573
+ console.log(JSON.stringify(getRetentionPolicy(phrenPath), null, 2));
574
+ const conflictOn = process.env.PHREN_FEATURE_SEMANTIC_CONFLICT === "1";
449
575
  process.stderr.write(`\nDedup: free Jaccard similarity scan on every add_finding (no API key needed).\n`);
450
576
  process.stderr.write(` Near-matches (30–55% overlap) are returned in the response for the agent to decide.\n`);
451
577
  if (conflictOn) {
@@ -458,9 +584,40 @@ export async function handleRetentionPolicy(args) {
458
584
  }
459
585
  return;
460
586
  }
461
- if (args[0] === "set") {
587
+ if (filteredArgs[0] === "set") {
588
+ if (projectArg) {
589
+ if (!isValidProjectName(projectArg)) {
590
+ console.error(`Invalid project name: "${projectArg}"`);
591
+ process.exit(1);
592
+ }
593
+ warnIfUnregistered(phrenPath, projectArg);
594
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => {
595
+ const existingRetention = current.retentionPolicy ?? {};
596
+ const retentionPatch = { ...existingRetention };
597
+ for (const arg of filteredArgs.slice(1)) {
598
+ if (!arg.startsWith("--"))
599
+ continue;
600
+ const [k, v] = arg.slice(2).split("=");
601
+ if (!k || v === undefined)
602
+ continue;
603
+ const num = Number(v);
604
+ const value = Number.isNaN(num) ? v : num;
605
+ if (k.startsWith("decay.")) {
606
+ retentionPatch.decay = { ...(retentionPatch.decay ?? {}) };
607
+ retentionPatch.decay[k.slice("decay.".length)] = value;
608
+ }
609
+ else {
610
+ retentionPatch[k] = value;
611
+ }
612
+ }
613
+ return { ...current, retentionPolicy: retentionPatch };
614
+ });
615
+ const resolved = mergeConfig(phrenPath, projectArg);
616
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.retentionPolicy }, null, 2));
617
+ return;
618
+ }
462
619
  const patch = {};
463
- for (const arg of args.slice(1)) {
620
+ for (const arg of filteredArgs.slice(1)) {
464
621
  if (!arg.startsWith("--"))
465
622
  continue;
466
623
  const [k, v] = arg.slice(2).split("=");
@@ -476,7 +633,7 @@ export async function handleRetentionPolicy(args) {
476
633
  patch[k] = value;
477
634
  }
478
635
  }
479
- const result = updateRetentionPolicy(getPhrenPath(), patch);
636
+ const result = updateRetentionPolicy(phrenPath, patch);
480
637
  if (!result.ok) {
481
638
  console.log(result.error);
482
639
  if (result.code === "PERMISSION_DENIED")
@@ -486,18 +643,68 @@ export async function handleRetentionPolicy(args) {
486
643
  console.log(JSON.stringify(result.data, null, 2));
487
644
  return;
488
645
  }
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]");
646
+ 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
647
  process.exit(1);
491
648
  }
492
649
  // ── Memory workflow ──────────────────────────────────────────────────────────
493
650
  export async function handleWorkflowPolicy(args) {
494
- if (!args.length || args[0] === "get") {
495
- console.log(JSON.stringify(getWorkflowPolicy(getPhrenPath()), null, 2));
651
+ const phrenPath = getPhrenPath();
652
+ const { project: projectArg, rest: filteredArgs } = parseProjectArg(args);
653
+ if (!filteredArgs.length || filteredArgs[0] === "get") {
654
+ if (projectArg) {
655
+ if (!isValidProjectName(projectArg)) {
656
+ console.error(`Invalid project name: "${projectArg}"`);
657
+ process.exit(1);
658
+ }
659
+ const resolved = mergeConfig(phrenPath, projectArg);
660
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.workflowPolicy }, null, 2));
661
+ return;
662
+ }
663
+ console.log(JSON.stringify(getWorkflowPolicy(phrenPath), null, 2));
496
664
  return;
497
665
  }
498
- if (args[0] === "set") {
666
+ if (filteredArgs[0] === "set") {
667
+ if (projectArg) {
668
+ if (!isValidProjectName(projectArg)) {
669
+ console.error(`Invalid project name: "${projectArg}"`);
670
+ process.exit(1);
671
+ }
672
+ warnIfUnregistered(phrenPath, projectArg);
673
+ updateProjectConfigOverrides(phrenPath, projectArg, (current) => {
674
+ const nextConfig = { ...current };
675
+ const existingWorkflow = current.workflowPolicy ?? {};
676
+ const workflowPatch = { ...existingWorkflow };
677
+ for (const arg of filteredArgs.slice(1)) {
678
+ if (!arg.startsWith("--"))
679
+ continue;
680
+ const [k, v] = arg.slice(2).split("=");
681
+ if (!k || v === undefined)
682
+ continue;
683
+ if (k === "riskySections") {
684
+ workflowPatch.riskySections = v.split(",").map((s) => s.trim()).filter(Boolean);
685
+ }
686
+ else if (k === "taskMode") {
687
+ nextConfig.taskMode = v;
688
+ }
689
+ else if (k === "findingSensitivity") {
690
+ nextConfig.findingSensitivity = v;
691
+ }
692
+ else {
693
+ const num = Number(v);
694
+ workflowPatch[k] = Number.isNaN(num) ? v : num;
695
+ }
696
+ }
697
+ if (Object.keys(workflowPatch).length > 0 || existingWorkflow !== current.workflowPolicy) {
698
+ nextConfig.workflowPolicy = workflowPatch;
699
+ }
700
+ return nextConfig;
701
+ });
702
+ const resolved = mergeConfig(phrenPath, projectArg);
703
+ console.log(JSON.stringify({ _project: projectArg, ...resolved.workflowPolicy }, null, 2));
704
+ return;
705
+ }
499
706
  const patch = {};
500
- for (const arg of args.slice(1)) {
707
+ for (const arg of filteredArgs.slice(1)) {
501
708
  if (!arg.startsWith("--"))
502
709
  continue;
503
710
  const [k, v] = arg.slice(2).split("=");
@@ -511,7 +718,7 @@ export async function handleWorkflowPolicy(args) {
511
718
  patch[k] = Number.isNaN(num) ? v : num;
512
719
  }
513
720
  }
514
- const result = updateWorkflowPolicy(getPhrenPath(), patch);
721
+ const result = updateWorkflowPolicy(phrenPath, patch);
515
722
  if (!result.ok) {
516
723
  console.log(result.error);
517
724
  if (result.code === "PERMISSION_DENIED")
@@ -521,7 +728,7 @@ export async function handleWorkflowPolicy(args) {
521
728
  console.log(JSON.stringify(result.data, null, 2));
522
729
  return;
523
730
  }
524
- console.error("Usage: phren config workflow [get|set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts --taskMode=manual]");
731
+ console.error("Usage: phren config workflow [--project <name>] [get|set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts --taskMode=manual]");
525
732
  process.exit(1);
526
733
  }
527
734
  // ── Machines and profiles ────────────────────────────────────────────────────