@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
@@ -236,7 +236,7 @@ export function register(server, ctx) {
236
236
  extraAnnotationsByFinding.push(conflicts.checked && conflicts.annotations.length > 0 ? conflicts.annotations : []);
237
237
  }
238
238
  catch (err) {
239
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
239
+ if ((process.env.PHREN_DEBUG))
240
240
  process.stderr.write(`[phren] add_findings semanticConflict: ${errorMessage(err)}\n`);
241
241
  extraAnnotationsByFinding.push([]);
242
242
  }
@@ -499,7 +499,7 @@ export function register(server, ctx) {
499
499
  hasRemote = remotes.length > 0;
500
500
  }
501
501
  catch (err) {
502
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
502
+ if ((process.env.PHREN_DEBUG))
503
503
  process.stderr.write(`[phren] push_changes remoteCheck: ${errorMessage(err)}\n`);
504
504
  }
505
505
  if (!hasRemote) {
@@ -523,7 +523,7 @@ export function register(server, ctx) {
523
523
  runGit(["pull", "--rebase", "--quiet"], { timeout: 15000 });
524
524
  }
525
525
  catch (pullErr) {
526
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
526
+ if ((process.env.PHREN_DEBUG))
527
527
  process.stderr.write(`[phren] push_changes pullRebase: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}\n`);
528
528
  const resolved = autoMergeConflicts(phrenPath);
529
529
  if (resolved) {
@@ -534,13 +534,13 @@ export function register(server, ctx) {
534
534
  });
535
535
  }
536
536
  catch (continueErr) {
537
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
537
+ if ((process.env.PHREN_DEBUG))
538
538
  process.stderr.write(`[phren] push_changes rebaseContinue: ${continueErr instanceof Error ? continueErr.message : String(continueErr)}\n`);
539
539
  try {
540
540
  runGit(["rebase", "--abort"]);
541
541
  }
542
542
  catch (abortErr) {
543
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
543
+ if ((process.env.PHREN_DEBUG))
544
544
  process.stderr.write(`[phren] push_changes rebaseAbort: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
545
545
  }
546
546
  break;
@@ -551,7 +551,7 @@ export function register(server, ctx) {
551
551
  runGit(["rebase", "--abort"]);
552
552
  }
553
553
  catch (abortErr) {
554
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
554
+ if ((process.env.PHREN_DEBUG))
555
555
  process.stderr.write(`[phren] push_changes rebaseAbort2: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
556
556
  }
557
557
  break;
@@ -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 crypto from "crypto";
5
- import { isValidProjectName } from "./utils.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { queryDocBySourceKey, queryRows, queryFragmentLinks, queryCrossProjectFragments, ensureGlobalEntitiesTable, logFragmentMiss } from "./shared-index.js";
7
7
  import { runtimeFile } from "./shared.js";
8
8
  import { withFileLock } from "./shared-governance.js";
@@ -209,8 +209,8 @@ export function register(server, ctx) {
209
209
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [fragmentName, resolvedFragmentType, new Date().toISOString().slice(0, 10)]);
210
210
  }
211
211
  catch (err) {
212
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
213
- process.stderr.write(`[phren] link_findings fragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
212
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
213
+ process.stderr.write(`[phren] link_findings fragmentInsert: ${errorMessage(err)}\n`);
214
214
  }
215
215
  const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [fragmentName, resolvedFragmentType]);
216
216
  if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
@@ -232,8 +232,8 @@ export function register(server, ctx) {
232
232
  db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
233
233
  }
234
234
  catch (err) {
235
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
236
- process.stderr.write(`[phren] link_findings docFragmentInsert: ${err instanceof Error ? err.message : String(err)}\n`);
235
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
236
+ process.stderr.write(`[phren] link_findings docFragmentInsert: ${errorMessage(err)}\n`);
237
237
  }
238
238
  const docFragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
239
239
  if (!docFragmentResult?.length || !docFragmentResult[0]?.values?.length) {
@@ -245,8 +245,8 @@ export function register(server, ctx) {
245
245
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, relType, sourceDoc]);
246
246
  }
247
247
  catch (err) {
248
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
249
- process.stderr.write(`[phren] link_findings linkInsert: ${err instanceof Error ? err.message : String(err)}\n`);
248
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
249
+ process.stderr.write(`[phren] link_findings linkInsert: ${errorMessage(err)}\n`);
250
250
  return mcpResponse({ ok: false, error: "Failed to insert fragment link." });
251
251
  }
252
252
  // 4a. Also populate global_entities so manual links appear in cross_project_fragments
@@ -255,8 +255,8 @@ export function register(server, ctx) {
255
255
  db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [fragmentName, project, sourceDoc]);
256
256
  }
257
257
  catch (err) {
258
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
259
- process.stderr.write(`[phren] link_findings globalFragments: ${err instanceof Error ? err.message : String(err)}\n`);
258
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
259
+ process.stderr.write(`[phren] link_findings globalFragments: ${errorMessage(err)}\n`);
260
260
  }
261
261
  // 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation)
262
262
  const manualLinksPath = runtimeFile(ctx.phrenPath, "manual-links.json");
@@ -268,8 +268,8 @@ export function register(server, ctx) {
268
268
  existing = JSON.parse(fs.readFileSync(manualLinksPath, "utf8"));
269
269
  }
270
270
  catch (err) {
271
- if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
272
- process.stderr.write(`[phren] link_findings manualLinksRead: ${err instanceof Error ? err.message : String(err)}\n`);
271
+ if (process.env.PHREN_DEBUG || (process.env.PHREN_DEBUG))
272
+ process.stderr.write(`[phren] link_findings manualLinksRead: ${errorMessage(err)}\n`);
273
273
  }
274
274
  }
275
275
  const newEntry = { entity: fragmentName, entityType: resolvedFragmentType, sourceDoc, relType };
@@ -4,7 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { runtimeFile, getProjectDirs } from "./shared.js";
6
6
  import { findFtsCacheForPath } from "./shared-index.js";
7
- import { isValidProjectName } from "./utils.js";
7
+ import { isValidProjectName, errorMessage } from "./utils.js";
8
8
  import { readReviewQueue, readReviewQueueAcrossProjects } from "./data-access.js";
9
9
  import { addProjectFromPath } from "./core-project.js";
10
10
  import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "./project-config.js";
@@ -44,7 +44,7 @@ export function register(server, ctx) {
44
44
  catch (err) {
45
45
  return mcpResponse({
46
46
  ok: false,
47
- error: err instanceof Error ? err.message : String(err),
47
+ error: errorMessage(err),
48
48
  });
49
49
  }
50
50
  });
@@ -108,8 +108,8 @@ export function register(server, ctx) {
108
108
  version = pkg.version || "unknown";
109
109
  }
110
110
  catch (err) {
111
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
112
- process.stderr.write(`[phren] healthCheck version: ${err instanceof Error ? err.message : String(err)}\n`);
111
+ if ((process.env.PHREN_DEBUG))
112
+ process.stderr.write(`[phren] healthCheck version: ${errorMessage(err)}\n`);
113
113
  }
114
114
  // FTS index (lives in /tmpphren-fts-*/, not .runtime/)
115
115
  let indexStatus = { exists: false };
@@ -117,8 +117,8 @@ export function register(server, ctx) {
117
117
  indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
118
118
  }
119
119
  catch (err) {
120
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
121
- process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${err instanceof Error ? err.message : String(err)}\n`);
120
+ if ((process.env.PHREN_DEBUG))
121
+ process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${errorMessage(err)}\n`);
122
122
  }
123
123
  // Hook registration
124
124
  let hooksEnabled = false;
@@ -127,8 +127,8 @@ export function register(server, ctx) {
127
127
  hooksEnabled = getHooksEnabledPreference(phrenPath);
128
128
  }
129
129
  catch (err) {
130
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
131
- process.stderr.write(`[phren] healthCheck hooksEnabled: ${err instanceof Error ? err.message : String(err)}\n`);
130
+ if ((process.env.PHREN_DEBUG))
131
+ process.stderr.write(`[phren] healthCheck hooksEnabled: ${errorMessage(err)}\n`);
132
132
  }
133
133
  let mcpEnabled = false;
134
134
  try {
@@ -136,8 +136,8 @@ export function register(server, ctx) {
136
136
  mcpEnabled = getMcpEnabledPreference(phrenPath);
137
137
  }
138
138
  catch (err) {
139
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
140
- process.stderr.write(`[phren] healthCheck mcpEnabled: ${err instanceof Error ? err.message : String(err)}\n`);
139
+ if ((process.env.PHREN_DEBUG))
140
+ process.stderr.write(`[phren] healthCheck mcpEnabled: ${errorMessage(err)}\n`);
141
141
  }
142
142
  // Profile/machine info
143
143
  const machineName = (() => {
@@ -145,8 +145,8 @@ export function register(server, ctx) {
145
145
  return getMachineName();
146
146
  }
147
147
  catch (err) {
148
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
149
- process.stderr.write(`[phren] healthCheck machineName: ${err instanceof Error ? err.message : String(err)}\n`);
148
+ if ((process.env.PHREN_DEBUG))
149
+ process.stderr.write(`[phren] healthCheck machineName: ${errorMessage(err)}\n`);
150
150
  }
151
151
  return undefined;
152
152
  })();
@@ -160,8 +160,8 @@ export function register(server, ctx) {
160
160
  taskMode = workflowPolicy.taskMode;
161
161
  }
162
162
  catch (err) {
163
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
164
- process.stderr.write(`[phren] healthCheck taskMode: ${err instanceof Error ? err.message : String(err)}\n`);
163
+ if ((process.env.PHREN_DEBUG))
164
+ process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
165
165
  }
166
166
  try {
167
167
  const { readInstallPreferences } = await import("./init-preferences.js");
@@ -169,8 +169,8 @@ export function register(server, ctx) {
169
169
  proactivity = prefs.proactivity || "high";
170
170
  }
171
171
  catch (err) {
172
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
173
- process.stderr.write(`[phren] healthCheck proactivity: ${err instanceof Error ? err.message : String(err)}\n`);
172
+ if ((process.env.PHREN_DEBUG))
173
+ process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
174
174
  }
175
175
  const lines = [
176
176
  `Phren v${version}`,
@@ -262,8 +262,8 @@ export function register(server, ctx) {
262
262
  return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
263
263
  }
264
264
  catch (err) {
265
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
266
- process.stderr.write(`[phren] readErrorLines: ${err instanceof Error ? err.message : String(err)}\n`);
265
+ if ((process.env.PHREN_DEBUG))
266
+ process.stderr.write(`[phren] readErrorLines: ${errorMessage(err)}\n`);
267
267
  return [];
268
268
  }
269
269
  }
@@ -35,8 +35,8 @@ export function logSearchMiss(phrenPath, query, project) {
35
35
  fs.appendFileSync(missFile, entry + "\n");
36
36
  }
37
37
  catch (err) {
38
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
39
- process.stderr.write(`[phren] logSearchMiss: ${err instanceof Error ? err.message : String(err)}\n`);
38
+ if ((process.env.PHREN_DEBUG))
39
+ process.stderr.write(`[phren] logSearchMiss: ${errorMessage(err)}\n`);
40
40
  }
41
41
  }
42
42
  const HISTORY_FINDING_STATUSES = new Set(["superseded", "retracted"]);
@@ -160,7 +160,7 @@ export function register(server, ctx) {
160
160
  createdAt = stat.birthtime.toISOString();
161
161
  }
162
162
  catch (err) {
163
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
163
+ if ((process.env.PHREN_DEBUG))
164
164
  process.stderr.write(`[phren] search_knowledge statFile: ${errorMessage(err)}\n`);
165
165
  }
166
166
  // Extract tags from content (e.g. [decision], [pitfall], [pattern])
@@ -393,8 +393,8 @@ export function register(server, ctx) {
393
393
  relatedFragments = [...new Set(relatedFragments)].slice(0, 10);
394
394
  }
395
395
  catch (err) {
396
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
397
- process.stderr.write(`[phren] fragment query: ${err instanceof Error ? err.message : String(err)}\n`);
396
+ if ((process.env.PHREN_DEBUG))
397
+ process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
398
398
  }
399
399
  const formatted = results.map((r) => `### ${r.project}/${r.filename} (${r.type})\n${r.snippet}\n\n\`${r.path}\``);
400
400
  // Memory synthesis: generate a concise paragraph from top results when requested
@@ -408,7 +408,7 @@ export function register(server, ctx) {
408
408
  synthCache = JSON.parse(fs.readFileSync(synthCachePath, "utf8"));
409
409
  }
410
410
  catch (err) {
411
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
411
+ if ((process.env.PHREN_DEBUG))
412
412
  process.stderr.write(`[phren] search_knowledge synthCacheRead: ${errorMessage(err)}\n`);
413
413
  }
414
414
  const cached = synthCache[synthKey];
@@ -433,8 +433,8 @@ export function register(server, ctx) {
433
433
  fs.writeFileSync(synthCachePath, JSON.stringify(synthCache));
434
434
  }
435
435
  catch (err) {
436
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
437
- process.stderr.write(`[phren] synthCache write: ${err instanceof Error ? err.message : String(err)}\n`);
436
+ if ((process.env.PHREN_DEBUG))
437
+ process.stderr.write(`[phren] synthCache write: ${errorMessage(err)}\n`);
438
438
  }
439
439
  }
440
440
  }
@@ -267,6 +267,17 @@ ${TASK_UI_STYLES}
267
267
  <div id="tab-settings" class="tab-content">
268
268
  <div class="settings-shell">
269
269
  <div id="settings-status-inline" class="settings-status-inline" aria-live="polite"></div>
270
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--cyan) 45%, var(--border))">
271
+ <div class="settings-section-header" style="display:flex;align-items:center;justify-content:space-between;gap:16px">
272
+ <span>Scope</span>
273
+ <select id="settings-project-select" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;background:var(--surface);color:var(--ink);font-size:var(--text-sm);font-family:var(--font)">
274
+ <option value="">Global (all projects)</option>
275
+ </select>
276
+ </div>
277
+ <div class="settings-section-body" style="padding:12px 18px">
278
+ <div id="settings-scope-note" style="font-size:var(--text-sm);color:var(--muted)">Showing global settings. Select a project to view and edit per-project overrides.</div>
279
+ </div>
280
+ </section>
270
281
  <section class="settings-section settings-section-findings">
271
282
  <div class="settings-section-header">Findings</div>
272
283
  <div class="settings-section-body">
@@ -279,6 +290,18 @@ ${TASK_UI_STYLES}
279
290
  <div id="settings-behavior" style="color:var(--muted)">Loading...</div>
280
291
  </div>
281
292
  </section>
293
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--warning) 45%, var(--border))">
294
+ <div class="settings-section-header">Retention Policy</div>
295
+ <div class="settings-section-body">
296
+ <div id="settings-retention" style="color:var(--muted)">Loading...</div>
297
+ </div>
298
+ </section>
299
+ <section class="settings-section" style="border-top:3px solid color-mix(in srgb, var(--success) 45%, var(--border))">
300
+ <div class="settings-section-header">Workflow Policy</div>
301
+ <div class="settings-section-body">
302
+ <div id="settings-workflow" style="color:var(--muted)">Loading...</div>
303
+ </div>
304
+ </section>
282
305
  <section class="settings-section settings-section-integrations">
283
306
  <div class="settings-section-header">Integrations</div>
284
307
  <div class="settings-section-body">
@@ -839,14 +839,95 @@ export function renderTasksAndSettingsScript(authToken) {
839
839
  });
840
840
  }
841
841
 
842
+ function getSettingsProject() {
843
+ var sel = document.getElementById('settings-project-select');
844
+ return sel ? sel.value : '';
845
+ }
846
+
847
+ function postProjectOverride(project, field, value, clearField) {
848
+ var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
849
+ fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
850
+ var payload = { project: project, field: field, value: value || '', clear: clearField ? 'true' : 'false' };
851
+ var body = new URLSearchParams(payload);
852
+ if (csrfData.token) body.set('_csrf', csrfData.token);
853
+ var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
854
+ return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
855
+ }).then(function(r) { return r.json(); }).then(function(data) {
856
+ if (!data.ok) {
857
+ setSettingsStatus(data.error || 'Failed to update project override', 'err');
858
+ return;
859
+ }
860
+ _settingsLoaded = false;
861
+ loadSettings();
862
+ setSettingsStatus('Project override updated', 'ok');
863
+ }).catch(function(err) {
864
+ setSettingsStatus('Failed: ' + String(err), 'err');
865
+ });
866
+ }
867
+
842
868
  function loadSettings() {
843
- var url = _tsAuthToken ? tsAuthUrl('/api/settings') : '/api/settings';
869
+ var selectedProject = getSettingsProject();
870
+ var baseUrl = '/api/settings';
871
+ if (selectedProject) baseUrl += '?project=' + encodeURIComponent(selectedProject);
872
+ var url = _tsAuthToken ? tsAuthUrl(baseUrl) : baseUrl;
873
+
874
+ // Populate project selector on first load
875
+ var sel = document.getElementById('settings-project-select');
876
+ if (sel && sel.querySelectorAll('option[data-proj]').length === 0) {
877
+ var configUrl = _tsAuthToken ? tsAuthUrl('/api/config') : '/api/config';
878
+ fetch(configUrl).then(function(r) { return r.json(); }).then(function(d) {
879
+ if (d.ok && d.projects && d.projects.length && sel) {
880
+ d.projects.forEach(function(p) {
881
+ var opt = document.createElement('option');
882
+ opt.value = p; opt.textContent = p;
883
+ opt.setAttribute('data-proj', '1');
884
+ sel.appendChild(opt);
885
+ });
886
+ if (selectedProject) sel.value = selectedProject;
887
+ }
888
+ }).catch(function() {});
889
+ }
890
+
891
+ var scopeNote = document.getElementById('settings-scope-note');
892
+ if (scopeNote) {
893
+ scopeNote.textContent = selectedProject
894
+ ? 'Showing effective config for "' + selectedProject + '". Overrides are saved to that project\'s phren.project.yaml.'
895
+ : 'Showing global settings. Select a project to view and edit per-project overrides.';
896
+ }
897
+
898
+ // Wire onChange once
899
+ if (sel && !sel.getAttribute('data-onchange-wired')) {
900
+ sel.setAttribute('data-onchange-wired', '1');
901
+ sel.addEventListener('change', function() {
902
+ _settingsLoaded = false;
903
+ loadSettings();
904
+ });
905
+ }
906
+
844
907
  fetch(url).then(function(r) { return r.json(); }).then(function(data) {
845
908
  if (!data.ok) {
846
909
  setSettingsStatus(data.error || 'Failed to load settings', 'err');
847
910
  return;
848
911
  }
849
912
 
913
+ // Use merged config when a project is selected, else global
914
+ var effective = (selectedProject && data.merged) ? data.merged : null;
915
+ var rawOverrides = (selectedProject && data.overrides) ? data.overrides : null;
916
+ var effectiveSensitivity = effective ? effective.findingSensitivity : (data.findingSensitivity || 'balanced');
917
+ var effectiveTaskMode = effective ? effective.taskMode : (data.taskMode || 'auto');
918
+ var effectiveProactivity = data.proactivity || 'high';
919
+ var effectiveRetention = (effective && effective.retentionPolicy) ? effective.retentionPolicy : (data.retentionPolicy || {});
920
+ var effectiveWorkflow = (effective && effective.workflowPolicy) ? effective.workflowPolicy : (data.workflowPolicy || {});
921
+
922
+ var isProject = Boolean(selectedProject);
923
+
924
+ function sourceBadge(isOverride) {
925
+ if (!isProject) return '';
926
+ return isOverride
927
+ ? '<span style="font-size:10px;font-weight:600;color:var(--warning);margin-left:6px;padding:1px 6px;border:1px solid color-mix(in srgb,var(--warning) 40%,transparent);border-radius:var(--radius-sm)">project override</span>'
928
+ : '<span style="font-size:10px;color:var(--text-muted);margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:var(--radius-sm)">global default</span>';
929
+ }
930
+
850
931
  var findingDescriptions = {
851
932
  high: 'Capture findings proactively, including minor observations.',
852
933
  medium: 'Capture findings that are likely useful.',
@@ -856,56 +937,129 @@ export function renderTasksAndSettingsScript(authToken) {
856
937
 
857
938
  var findingsEl = document.getElementById('settings-findings');
858
939
  if (findingsEl) {
859
- var fsUi = findingStorageToUi(data.findingSensitivity || 'balanced');
940
+ var fsUi = findingStorageToUi(effectiveSensitivity);
860
941
  var findingsHtml = '';
942
+ var fsSensOverride = rawOverrides && rawOverrides.findingSensitivity != null;
861
943
  findingsHtml += '<div class="settings-control">';
862
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span></div>';
944
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span>' + sourceBadge(fsSensOverride);
945
+ if (isProject && fsSensOverride) {
946
+ findingsHtml += '<button data-ts-action="clearProjectOverride" data-field="findingSensitivity" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
947
+ }
948
+ findingsHtml += '</div>';
863
949
  findingsHtml += '<div class="settings-chip-row">';
864
950
  ['high', 'medium', 'low', 'minimal'].forEach(function(level) {
865
951
  var active = level === fsUi ? ' active' : '';
866
- findingsHtml += '<button data-ts-action="setFindingSensitivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
952
+ var action = isProject ? 'setProjectFindingSensitivity' : 'setFindingSensitivity';
953
+ findingsHtml += '<button data-ts-action="' + action + '" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
867
954
  });
868
955
  findingsHtml += '</div>';
869
956
  findingsHtml += '<div class="settings-control-note" id="settings-fs-desc">' + esc(findingDescriptions[fsUi] || '') + '</div>';
870
957
  findingsHtml += '</div>';
871
- findingsHtml += '<div class="settings-control">';
872
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
873
- findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
874
- findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
875
- findingsHtml += '</div>';
876
- findingsHtml += '<div class="settings-control">';
877
- findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
878
- findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
879
- findingsHtml += '</div>';
958
+ if (!isProject) {
959
+ findingsHtml += '<div class="settings-control">';
960
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
961
+ findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
962
+ findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
963
+ findingsHtml += '</div>';
964
+ findingsHtml += '<div class="settings-control">';
965
+ findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
966
+ findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
967
+ findingsHtml += '</div>';
968
+ }
880
969
  findingsEl.innerHTML = findingsHtml;
881
970
  }
882
971
 
883
972
  var behaviorEl = document.getElementById('settings-behavior');
884
973
  if (behaviorEl) {
885
- var taskMode = data.taskMode === 'suggest' ? 'manual' : (data.taskMode || 'auto');
886
- var proactivity = data.proactivity || 'high';
974
+ var taskMode = effectiveTaskMode || 'auto';
975
+ var proactivity = effectiveProactivity;
887
976
  var behaviorHtml = '';
977
+ var taskModeOverride = rawOverrides && rawOverrides.taskMode != null;
888
978
  behaviorHtml += '<div class="settings-control">';
889
- behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span></div>';
979
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span>' + sourceBadge(taskModeOverride);
980
+ if (isProject && taskModeOverride) {
981
+ behaviorHtml += '<button data-ts-action="clearProjectOverride" data-field="taskMode" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
982
+ }
983
+ behaviorHtml += '</div>';
890
984
  behaviorHtml += '<div class="settings-chip-row">';
891
- ['auto', 'manual', 'off'].forEach(function(mode) {
985
+ ['auto', 'suggest', 'manual', 'off'].forEach(function(mode) {
892
986
  var active = mode === taskMode ? ' active' : '';
893
- behaviorHtml += '<button data-ts-action="setTaskMode" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
894
- });
895
- behaviorHtml += '</div></div>';
896
- behaviorHtml += '<div class="settings-control">';
897
- behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
898
- behaviorHtml += '<div class="settings-chip-row">';
899
- ['high', 'medium', 'low'].forEach(function(level) {
900
- var active = level === proactivity ? ' active' : '';
901
- behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
987
+ var action = isProject ? 'setProjectTaskMode' : 'setTaskMode';
988
+ behaviorHtml += '<button data-ts-action="' + action + '" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
902
989
  });
903
990
  behaviorHtml += '</div></div>';
991
+ if (!isProject) {
992
+ behaviorHtml += '<div class="settings-control">';
993
+ behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
994
+ behaviorHtml += '<div class="settings-chip-row">';
995
+ ['high', 'medium', 'low'].forEach(function(level) {
996
+ var active = level === proactivity ? ' active' : '';
997
+ behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
998
+ });
999
+ behaviorHtml += '</div></div>';
1000
+ }
904
1001
  behaviorEl.innerHTML = behaviorHtml;
905
1002
  }
906
1003
 
1004
+ var retentionEl = document.getElementById('settings-retention');
1005
+ if (retentionEl) {
1006
+ var ret = effectiveRetention;
1007
+ var retHtml = '';
1008
+ function retRow(label, field, value, note) {
1009
+ var isOverride = isProject && rawOverrides && rawOverrides.retentionPolicy && rawOverrides.retentionPolicy[field] !== undefined;
1010
+ retHtml += '<div class="settings-control">';
1011
+ retHtml += '<div class="settings-control-header"><span class="settings-control-label">' + esc(label) + '</span>' + sourceBadge(isOverride);
1012
+ retHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(value != null ? value : '—')) + '</span>';
1013
+ if (isProject && isOverride) {
1014
+ retHtml += '<button data-ts-action="clearProjectOverride" data-field="' + esc(field) + '" class="settings-chip" style="font-size:11px">Clear</button>';
1015
+ }
1016
+ retHtml += '</div>';
1017
+ if (note) retHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
1018
+ if (isProject) {
1019
+ retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1020
+ '<input type="number" id="ret-input-' + esc(field) + '" value="' + esc(String(value != null ? value : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1021
+ '<button data-ts-action="setProjectRetention" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
1022
+ '</div>';
1023
+ }
1024
+ retHtml += '</div>';
1025
+ }
1026
+ retRow('TTL days', 'ttlDays', ret.ttlDays, 'Memories older than this are eligible for pruning.');
1027
+ retRow('Retention days', 'retentionDays', ret.retentionDays, 'Hard cutoff — memories past this age are removed.');
1028
+ retRow('Auto-accept threshold', 'autoAcceptThreshold', ret.autoAcceptThreshold, 'Confidence score (0–1) above which memories are auto-accepted.');
1029
+ retRow('Min inject confidence', 'minInjectConfidence', ret.minInjectConfidence, 'Minimum confidence score to inject a memory into context.');
1030
+ retentionEl.innerHTML = retHtml;
1031
+ }
1032
+
1033
+ var workflowEl = document.getElementById('settings-workflow');
1034
+ if (workflowEl) {
1035
+ var wf = effectiveWorkflow;
1036
+ var wfHtml = '';
1037
+ var lctOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && rawOverrides.workflowPolicy.lowConfidenceThreshold !== undefined;
1038
+ var riskySectionsOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && Array.isArray(rawOverrides.workflowPolicy.riskySections) && rawOverrides.workflowPolicy.riskySections.length > 0;
1039
+ wfHtml += '<div class="settings-control">';
1040
+ wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Low confidence threshold</span>' + sourceBadge(lctOverride);
1041
+ wfHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '—')) + '</span>';
1042
+ if (isProject && lctOverride) {
1043
+ wfHtml += '<button data-ts-action="clearProjectOverride" data-field="lowConfidenceThreshold" class="settings-chip" style="font-size:11px">Clear</button>';
1044
+ }
1045
+ if (isProject) {
1046
+ wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
1047
+ '<input type="number" id="wf-input-lowConfidenceThreshold" min="0" max="1" step="0.05" value="' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
1048
+ '<button data-ts-action="setProjectWorkflow" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
1049
+ '</div>';
1050
+ } else {
1051
+ wfHtml += '</div>';
1052
+ }
1053
+ wfHtml += '<div class="settings-control-note">Memories below this confidence score are flagged for review.</div></div>';
1054
+ wfHtml += '<div class="settings-control">';
1055
+ wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Risky sections</span>' + sourceBadge(riskySectionsOverride);
1056
+ wfHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(Array.isArray(wf.riskySections) ? wf.riskySections.join(', ') : '—') + '</span></div>';
1057
+ wfHtml += '<div class="settings-control-note">Sections that trigger approval gates when memories are written.</div></div>';
1058
+ workflowEl.innerHTML = wfHtml;
1059
+ }
1060
+
907
1061
  var integrationsEl = document.getElementById('settings-integrations');
908
- if (integrationsEl) {
1062
+ if (integrationsEl && !isProject) {
909
1063
  var tools = Array.isArray(data.hookTools) ? data.hookTools : [];
910
1064
  var html = '';
911
1065
  html += '<div class="settings-control-header" style="margin-bottom:10px"><span class="settings-control-label">Global MCP</span>';
@@ -923,6 +1077,8 @@ export function renderTasksAndSettingsScript(authToken) {
923
1077
  });
924
1078
  html += '</tbody></table>';
925
1079
  integrationsEl.innerHTML = html;
1080
+ } else if (integrationsEl && isProject) {
1081
+ integrationsEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Integration settings are global — switch to Global scope to edit them.</div>';
926
1082
  }
927
1083
  }).catch(function(err) {
928
1084
  setSettingsStatus('Failed to load settings: ' + String(err), 'err');
@@ -1063,6 +1219,33 @@ export function renderTasksAndSettingsScript(authToken) {
1063
1219
  else if (action === 'toggleIntegrationTool') { toggleIntegrationTool(actionEl.getAttribute('data-tool')); }
1064
1220
  else if (action === 'showSessionDetail') { showSessionDetail(actionEl.getAttribute('data-session-id')); }
1065
1221
  else if (action === 'backToSessionsList') { backToSessionsList(); }
1222
+ else if (action === 'setProjectFindingSensitivity') {
1223
+ var proj = getSettingsProject();
1224
+ var level = actionEl.getAttribute('data-level');
1225
+ postProjectOverride(proj, 'findingSensitivity', findingUiToStorage(level || 'medium'), false);
1226
+ }
1227
+ else if (action === 'setProjectTaskMode') {
1228
+ var proj = getSettingsProject();
1229
+ postProjectOverride(proj, 'taskMode', actionEl.getAttribute('data-mode') || 'auto', false);
1230
+ }
1231
+ else if (action === 'clearProjectOverride') {
1232
+ var proj = getSettingsProject();
1233
+ postProjectOverride(proj, actionEl.getAttribute('data-field') || '', '', true);
1234
+ }
1235
+ else if (action === 'setProjectRetention') {
1236
+ var proj = getSettingsProject();
1237
+ var field = actionEl.getAttribute('data-field') || '';
1238
+ var inputEl = document.getElementById('ret-input-' + field);
1239
+ var val = inputEl ? inputEl.value : '';
1240
+ postProjectOverride(proj, field, val, false);
1241
+ }
1242
+ else if (action === 'setProjectWorkflow') {
1243
+ var proj = getSettingsProject();
1244
+ var field = actionEl.getAttribute('data-field') || '';
1245
+ var inputEl = document.getElementById('wf-input-' + field);
1246
+ var val = inputEl ? inputEl.value : '';
1247
+ postProjectOverride(proj, field, val, false);
1248
+ }
1066
1249
  });
1067
1250
 
1068
1251
  window.setFindingSensitivity = function(level) {