@phren/cli 0.0.9 → 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 (67) 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 +140 -3
  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 +41 -34
  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-citation.js +12 -22
  13. package/mcp/dist/content-dedup.js +9 -9
  14. package/mcp/dist/data-access.js +1 -1
  15. package/mcp/dist/data-tasks.js +23 -0
  16. package/mcp/dist/embedding.js +7 -7
  17. package/mcp/dist/entrypoint.js +129 -102
  18. package/mcp/dist/governance-locks.js +6 -5
  19. package/mcp/dist/governance-policy.js +155 -2
  20. package/mcp/dist/governance-scores.js +3 -3
  21. package/mcp/dist/hooks.js +39 -18
  22. package/mcp/dist/index.js +4 -4
  23. package/mcp/dist/init-config.js +3 -24
  24. package/mcp/dist/init-setup.js +5 -5
  25. package/mcp/dist/init.js +170 -23
  26. package/mcp/dist/link-checksums.js +3 -2
  27. package/mcp/dist/link-context.js +1 -1
  28. package/mcp/dist/link-doctor.js +3 -3
  29. package/mcp/dist/link-skills.js +98 -12
  30. package/mcp/dist/link.js +17 -27
  31. package/mcp/dist/machine-identity.js +1 -9
  32. package/mcp/dist/mcp-config.js +247 -42
  33. package/mcp/dist/mcp-data.js +9 -9
  34. package/mcp/dist/mcp-extract-facts.js +1 -1
  35. package/mcp/dist/mcp-extract.js +2 -2
  36. package/mcp/dist/mcp-finding.js +6 -6
  37. package/mcp/dist/mcp-graph.js +11 -11
  38. package/mcp/dist/mcp-ops.js +18 -18
  39. package/mcp/dist/mcp-search.js +8 -8
  40. package/mcp/dist/mcp-tasks.js +21 -1
  41. package/mcp/dist/memory-ui-page.js +23 -0
  42. package/mcp/dist/memory-ui-scripts.js +210 -27
  43. package/mcp/dist/memory-ui-server.js +115 -3
  44. package/mcp/dist/phren-paths.js +7 -7
  45. package/mcp/dist/profile-store.js +2 -2
  46. package/mcp/dist/project-config.js +63 -16
  47. package/mcp/dist/session-utils.js +3 -2
  48. package/mcp/dist/shared-fragment-graph.js +22 -21
  49. package/mcp/dist/shared-index.js +144 -105
  50. package/mcp/dist/shared-retrieval.js +22 -56
  51. package/mcp/dist/shared-search-fallback.js +13 -13
  52. package/mcp/dist/shared-sqljs.js +3 -2
  53. package/mcp/dist/shared.js +3 -3
  54. package/mcp/dist/shell-input.js +1 -1
  55. package/mcp/dist/shell-state-store.js +1 -1
  56. package/mcp/dist/shell-view.js +3 -2
  57. package/mcp/dist/shell.js +1 -1
  58. package/mcp/dist/skill-files.js +4 -10
  59. package/mcp/dist/skill-registry.js +3 -0
  60. package/mcp/dist/status.js +41 -13
  61. package/mcp/dist/task-hygiene.js +1 -1
  62. package/mcp/dist/telemetry.js +5 -4
  63. package/mcp/dist/update.js +1 -1
  64. package/mcp/dist/utils.js +3 -3
  65. package/package.json +2 -2
  66. package/starter/global/skills/audit.md +106 -0
  67. package/mcp/dist/shared-paths.js +0 -1
@@ -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
  }
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import { isValidProjectName } from "./utils.js";
6
- import { addTask as addTaskStore, addTasks as addTasksBatch, taskMarkdown, completeTask as completeTaskStore, completeTasks as completeTasksBatch, removeTask as removeTaskStore, linkTaskIssue, pinTask, workNextTask, tidyDoneTasks, readTasks, readTasksAcrossProjects, resolveTaskItem, TASKS_FILENAME, updateTask as updateTaskStore, promoteTask, } from "./data-access.js";
6
+ import { addTask as addTaskStore, addTasks as addTasksBatch, taskMarkdown, completeTask as completeTaskStore, completeTasks as completeTasksBatch, removeTask as removeTaskStore, removeTasks as removeTasksBatch, linkTaskIssue, pinTask, workNextTask, tidyDoneTasks, readTasks, readTasksAcrossProjects, resolveTaskItem, TASKS_FILENAME, updateTask as updateTaskStore, promoteTask, } from "./data-access.js";
7
7
  import { applyGravity } from "./data-tasks.js";
8
8
  import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo, } from "./tasks-github.js";
9
9
  import { clearTaskCheckpoint } from "./session-checkpoints.js";
@@ -327,6 +327,26 @@ export function register(server, ctx) {
327
327
  return mcpResponse({ ok: true, message: result.data, data: { project, item } });
328
328
  });
329
329
  });
330
+ server.registerTool("remove_tasks", {
331
+ title: "◆ phren · remove tasks (bulk)",
332
+ description: "Remove multiple tasks in one call. Pass an array of partial item texts or IDs.",
333
+ inputSchema: z.object({
334
+ project: z.string().describe("Project name."),
335
+ items: z.array(z.string()).describe("List of partial item texts or IDs to remove."),
336
+ }),
337
+ }, async ({ project, items }) => {
338
+ if (!isValidProjectName(project))
339
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
340
+ return withWriteQueue(async () => {
341
+ const result = removeTasksBatch(phrenPath, project, items);
342
+ if (!result.ok)
343
+ return mcpResponse({ ok: false, error: result.error });
344
+ const { removed, errors } = result.data;
345
+ if (removed.length > 0)
346
+ refreshTaskIndex(updateFileInIndex, phrenPath, project);
347
+ return mcpResponse({ ok: removed.length > 0, ...(removed.length === 0 ? { error: `No tasks removed: ${errors.join("; ")}` } : {}), message: `Removed ${removed.length}/${items.length} items`, data: { project, removed, errors } });
348
+ });
349
+ });
330
350
  server.registerTool("update_task", {
331
351
  title: "◆ phren · update task",
332
352
  description: "Update a task's text, priority, context, section, or GitHub metadata by matching text.",
@@ -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) {