@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
@@ -144,7 +144,7 @@ export async function handlePruneMemories(args = []) {
144
144
  .filter((e) => e !== null);
145
145
  }
146
146
  catch (err) {
147
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
147
+ if ((process.env.PHREN_DEBUG))
148
148
  process.stderr.write(`[phren] cli-govern retrievalLog readParse: ${errorMessage(err)}\n`);
149
149
  }
150
150
  }
@@ -227,6 +227,139 @@ export async function handleConsolidateMemories(args = []) {
227
227
  return;
228
228
  console.log(`Updated backups (${backups.length}): ${backups.join(", ")}`);
229
229
  }
230
+ export async function handleGcMaintain(args = []) {
231
+ const dryRun = args.includes("--dry-run");
232
+ const phrenPath = getPhrenPath();
233
+ const { execSync } = await import("child_process");
234
+ const report = {
235
+ gitGcRan: false,
236
+ commitsSquashed: 0,
237
+ sessionsRemoved: 0,
238
+ runtimeLogsRemoved: 0,
239
+ };
240
+ // 1. Run git gc --aggressive on the ~/.phren repo
241
+ if (dryRun) {
242
+ console.log("[dry-run] Would run: git gc --aggressive");
243
+ }
244
+ else {
245
+ try {
246
+ execSync("git gc --aggressive --quiet", { cwd: phrenPath, stdio: "pipe" });
247
+ report.gitGcRan = true;
248
+ console.log("git gc --aggressive: done");
249
+ }
250
+ catch (err) {
251
+ console.error(`git gc failed: ${errorMessage(err)}`);
252
+ }
253
+ }
254
+ // 2. Squash old auto-save commits (older than 7 days) into weekly summaries
255
+ const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
256
+ let oldCommits = [];
257
+ try {
258
+ const raw = execSync(`git log --before="${sevenDaysAgo}" --format="%H %ci %s"`, { cwd: phrenPath, encoding: "utf8" }).trim();
259
+ if (raw) {
260
+ oldCommits = raw.split("\n").filter((l) => l.includes("auto-save:") || l.includes("[auto]"));
261
+ }
262
+ }
263
+ catch {
264
+ // Not a git repo or no commits — skip silently
265
+ }
266
+ if (oldCommits.length === 0) {
267
+ console.log("Commit squash: no old auto-save commits to squash.");
268
+ }
269
+ else if (dryRun) {
270
+ console.log(`[dry-run] Would squash ${oldCommits.length} auto-save commits older than 7 days into weekly summaries.`);
271
+ report.commitsSquashed = oldCommits.length;
272
+ }
273
+ else {
274
+ // Group by ISO week based on commit timestamp (already in the log output)
275
+ const commitsByWeek = new Map();
276
+ for (const line of oldCommits) {
277
+ const hash = line.split(" ")[0];
278
+ // Format: "HASH YYYY-MM-DD HH:MM:SS +ZZZZ subject..."
279
+ const dateMatch = line.match(/^[a-f0-9]+ (\d{4}-\d{2}-\d{2})/);
280
+ if (!dateMatch)
281
+ continue;
282
+ const date = new Date(dateMatch[1]);
283
+ const weekStart = new Date(date);
284
+ weekStart.setDate(date.getDate() - date.getDay());
285
+ const weekKey = weekStart.toISOString().slice(0, 10);
286
+ if (!commitsByWeek.has(weekKey))
287
+ commitsByWeek.set(weekKey, []);
288
+ commitsByWeek.get(weekKey).push(hash);
289
+ }
290
+ // For each week with multiple commits, soft-reset to oldest and amend into a summary
291
+ for (const [weekKey, hashes] of commitsByWeek.entries()) {
292
+ if (hashes.length < 2)
293
+ continue;
294
+ try {
295
+ const oldest = hashes[hashes.length - 1];
296
+ const newest = hashes[0];
297
+ // Use git rebase --onto to squash: squash all into the oldest parent
298
+ const parentOfOldest = execSync(`git rev-parse ${oldest}^`, { cwd: phrenPath, encoding: "utf8" }).trim();
299
+ // Build rebase script via env variable to squash all but first to "squash"
300
+ const rebaseScript = hashes
301
+ .map((h, i) => `${i === hashes.length - 1 ? "pick" : "squash"} ${h} auto-save`)
302
+ .reverse()
303
+ .join("\n");
304
+ const scriptPath = path.join(phrenPath, ".runtime", `gc-rebase-${weekKey}.sh`);
305
+ fs.writeFileSync(scriptPath, rebaseScript);
306
+ // Use GIT_SEQUENCE_EDITOR to feed our script
307
+ execSync(`GIT_SEQUENCE_EDITOR="cat ${scriptPath} >" git rebase -i ${parentOfOldest}`, { cwd: phrenPath, stdio: "pipe" });
308
+ fs.unlinkSync(scriptPath);
309
+ report.commitsSquashed += hashes.length - 1;
310
+ console.log(`Squashed ${hashes.length} auto-save commits for week of ${weekKey} (${newest.slice(0, 7)}..${oldest.slice(0, 7)}).`);
311
+ }
312
+ catch {
313
+ // Squashing is best-effort — log and continue
314
+ console.warn(` Could not squash auto-save commits for week ${weekKey} (possibly non-linear history). Skipping.`);
315
+ }
316
+ }
317
+ if (report.commitsSquashed === 0) {
318
+ console.log("Commit squash: all old auto-save weeks have only one commit, nothing to squash.");
319
+ }
320
+ }
321
+ // 3–4. Prune stale files from .sessions/ and .runtime/
322
+ const thirtyDaysAgo = Date.now() - 30 * 86400000;
323
+ function pruneStaleFiles(dir, label, filter) {
324
+ let removed = 0;
325
+ if (!fs.existsSync(dir))
326
+ return removed;
327
+ for (const entry of fs.readdirSync(dir)) {
328
+ if (filter && !filter(entry))
329
+ continue;
330
+ const fullPath = path.join(dir, entry);
331
+ try {
332
+ if (fs.statSync(fullPath).mtimeMs < thirtyDaysAgo) {
333
+ if (dryRun) {
334
+ console.log(`[dry-run] Would remove: ${label}/${entry}`);
335
+ }
336
+ else {
337
+ fs.unlinkSync(fullPath);
338
+ }
339
+ removed++;
340
+ }
341
+ }
342
+ catch { /* skip unreadable */ }
343
+ }
344
+ return removed;
345
+ }
346
+ const logExtensions = new Set([".log", ".jsonl", ".json"]);
347
+ const protectedFiles = new Set(["audit.log", "telemetry.json"]);
348
+ report.sessionsRemoved = pruneStaleFiles(path.join(phrenPath, ".sessions"), ".sessions");
349
+ report.runtimeLogsRemoved = pruneStaleFiles(path.join(phrenPath, ".runtime"), ".runtime", (entry) => logExtensions.has(path.extname(entry)) && !protectedFiles.has(entry));
350
+ const verb = dryRun ? "Would remove" : "Removed";
351
+ console.log(`${verb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
352
+ console.log(`${verb} ${report.runtimeLogsRemoved} stale runtime log(s) from .runtime/`);
353
+ // 5. Summary
354
+ if (!dryRun) {
355
+ appendAuditLog(phrenPath, "maintain_gc", `gitGc=${report.gitGcRan} squashed=${report.commitsSquashed} sessions=${report.sessionsRemoved} logs=${report.runtimeLogsRemoved}`);
356
+ }
357
+ console.log(`\nGC complete:${dryRun ? " (dry-run)" : ""}` +
358
+ ` git_gc=${report.gitGcRan}` +
359
+ ` commits_squashed=${report.commitsSquashed}` +
360
+ ` sessions_pruned=${report.sessionsRemoved}` +
361
+ ` logs_pruned=${report.runtimeLogsRemoved}`);
362
+ }
230
363
  // ── Maintain router ──────────────────────────────────────────────────────────
231
364
  export async function handleMaintain(args) {
232
365
  const sub = args[0];
@@ -245,6 +378,8 @@ export async function handleMaintain(args) {
245
378
  return handleExtractMemories(rest[0]);
246
379
  case "restore":
247
380
  return handleRestoreBackup(rest);
381
+ case "gc":
382
+ return handleGcMaintain(rest);
248
383
  default:
249
384
  console.log(`phren maintain - memory maintenance and governance
250
385
 
@@ -258,7 +393,9 @@ Subcommands:
258
393
  Deduplicate FINDINGS.md bullets. Run after a burst of work
259
394
  when findings feel repetitive, or monthly to keep things clean.
260
395
  phren maintain extract [project] Mine git/GitHub signals into memory candidates
261
- phren maintain restore [project] List and restore from .bak files`);
396
+ phren maintain restore [project] List and restore from .bak files
397
+ phren maintain gc [--dry-run] Garbage-collect the ~/.phren repo: git gc, squash old
398
+ auto-save commits, prune stale session markers and runtime logs`);
262
399
  if (sub) {
263
400
  console.error(`\nUnknown maintain subcommand: "${sub}"`);
264
401
  process.exit(1);
@@ -364,7 +501,7 @@ export async function handleBackgroundMaintenance(projectArg) {
364
501
  fs.unlinkSync(markers.lock);
365
502
  }
366
503
  catch (err) {
367
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
504
+ if ((process.env.PHREN_DEBUG))
368
505
  process.stderr.write(`[phren] cli-govern backgroundMaintenance unlockFinal: ${errorMessage(err)}\n`);
369
506
  }
370
507
  }
@@ -1,6 +1,7 @@
1
1
  import { getPhrenPath } from "./shared.js";
2
2
  import { buildIndex, queryRows } from "./shared-index.js";
3
3
  import { resolveRuntimeProfile } from "./runtime-profile.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  /**
5
6
  * CLI: phren graph [--project <name>] [--limit <n>]
6
7
  * Displays the fragment knowledge graph as a table.
@@ -115,7 +116,7 @@ export async function handleGraphLink(args) {
115
116
  db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, "mentions", sourceDoc]);
116
117
  }
117
118
  catch (err) {
118
- console.error(`Failed to link: ${err instanceof Error ? err.message : String(err)}`);
119
+ console.error(`Failed to link: ${errorMessage(err)}`);
119
120
  process.exit(1);
120
121
  }
121
122
  // Persist to manual-links.json
@@ -144,7 +145,7 @@ export async function handleGraphLink(args) {
144
145
  });
145
146
  }
146
147
  catch (err) {
147
- console.error(`Failed to persist manual link: ${err instanceof Error ? err.message : String(err)}`);
148
+ console.error(`Failed to persist manual link: ${errorMessage(err)}`);
148
149
  process.exit(1);
149
150
  }
150
151
  console.log(`Linked "${fragmentName}" to ${sourceDoc}.`);
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { capCache } from "./shared.js";
4
+ import { errorMessage } from "./utils.js";
4
5
  // ── Glob matching and project frontmatter ────────────────────────────────────
5
6
  const projectGlobCache = new Map();
6
7
  export function clearProjectGlobCache() {
@@ -45,7 +46,7 @@ function parseProjectGlobs(phrenPathLocal, project) {
45
46
  }
46
47
  catch (err) {
47
48
  if (process.env.PHREN_DEBUG)
48
- process.stderr.write(`[phren] getProjectGlobs: ${err instanceof Error ? err.message : String(err)}\n`);
49
+ process.stderr.write(`[phren] getProjectGlobs: ${errorMessage(err)}\n`);
49
50
  }
50
51
  projectGlobCache.set(project, globs);
51
52
  capCache(projectGlobCache);
@@ -1,7 +1,7 @@
1
1
  import { recordInjection, recordRetrieval, } from "./shared-governance.js";
2
2
  import { getDocSourceKey, } from "./shared-index.js";
3
3
  import { logImpact, extractFindingIdsFromSnippet, } from "./finding-impact.js";
4
- import { isFeatureEnabled } from "./utils.js";
4
+ import { isFeatureEnabled, errorMessage } from "./utils.js";
5
5
  import { annotateStale } from "./cli-hooks-citations.js";
6
6
  import { approximateTokens, fileRelevanceBoost, branchMatchBoost } from "./shared-retrieval.js";
7
7
  // ── Progressive disclosure helpers ────────────────────────────────────────────
@@ -56,7 +56,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
56
56
  }
57
57
  catch (err) {
58
58
  if (process.env.PHREN_DEBUG)
59
- process.stderr.write(`[phren] injectContext recordRetrieval: ${err instanceof Error ? err.message : String(err)}\n`);
59
+ process.stderr.write(`[phren] injectContext recordRetrieval: ${errorMessage(err)}\n`);
60
60
  }
61
61
  }
62
62
  }
@@ -108,7 +108,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
108
108
  }
109
109
  catch (err) {
110
110
  if (process.env.PHREN_DEBUG)
111
- process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${err instanceof Error ? err.message : String(err)}\n`);
111
+ process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${errorMessage(err)}\n`);
112
112
  }
113
113
  parts.push(`[${getDocSourceKey(doc, phrenPathLocal)}] (${doc.type})`);
114
114
  parts.push(annotateStale(snippet));
@@ -5,7 +5,7 @@
5
5
  // cli-hooks-output.ts — hook output formatting
6
6
  // cli-hooks-globs.ts — project glob matching
7
7
  import { debugLog, sessionMarker, sessionsDir, getPhrenPath, } from "./shared.js";
8
- import { getRetentionPolicy, getWorkflowPolicy, } from "./shared-governance.js";
8
+ import { mergeConfig, } from "./shared-governance.js";
9
9
  import { buildIndex, detectProject, } from "./shared-index.js";
10
10
  import { isProjectHookEnabled } from "./project-config.js";
11
11
  import { checkConsolidationNeeded, } from "./shared-content.js";
@@ -126,7 +126,7 @@ export async function handleHookPrompt() {
126
126
  }
127
127
  catch (err) {
128
128
  if (process.env.PHREN_DEBUG)
129
- process.stderr.write(`[phren] hookPrompt stdinRead: ${err instanceof Error ? err.message : String(err)}\n`);
129
+ process.stderr.write(`[phren] hookPrompt stdinRead: ${errorMessage(err)}\n`);
130
130
  process.exit(0);
131
131
  }
132
132
  const input = parseHookInput(raw);
@@ -182,6 +182,7 @@ export async function handleHookPrompt() {
182
182
  appendAuditLog(getPhrenPath(), "hook_prompt", `status=project_disabled project=${detectedProject}`);
183
183
  process.exit(0);
184
184
  }
185
+ const resolvedConfig = mergeConfig(getPhrenPath(), detectedProject ?? undefined);
185
186
  const safeQuery = buildRobustFtsQuery(keywords, detectedProject, getPhrenPath());
186
187
  if (!safeQuery)
187
188
  process.exit(0);
@@ -193,14 +194,17 @@ export async function handleHookPrompt() {
193
194
  process.exit(0);
194
195
  autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
195
196
  const tTrust0 = Date.now();
196
- const policy = getRetentionPolicy(getPhrenPath());
197
+ const policy = resolvedConfig.retentionPolicy;
197
198
  const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
198
199
  const trustResult = applyTrustFilter(rows, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay, getPhrenPath());
199
200
  rows = trustResult.rows;
200
201
  stage.trustMs = Date.now() - tTrust0;
201
202
  if (!rows.length)
202
203
  process.exit(0);
203
- if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && getProactivityLevelForFindings(getPhrenPath()) !== "low" && sessionId && detectedProject && cwd) {
204
+ const findingsProactivity = resolvedConfig.proactivity.findings
205
+ ?? resolvedConfig.proactivity.base
206
+ ?? getProactivityLevelForFindings(getPhrenPath());
207
+ if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && findingsProactivity !== "low" && sessionId && detectedProject && cwd) {
204
208
  const marker = sessionMarker(getPhrenPath(), `extracted-${sessionId}-${detectedProject}`);
205
209
  if (!fs.existsSync(marker)) {
206
210
  try {
@@ -260,7 +264,9 @@ export async function handleHookPrompt() {
260
264
  debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
261
265
  }
262
266
  const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getPhrenPath(), sessionId);
263
- const taskLevel = getProactivityLevelForTask(getPhrenPath());
267
+ const taskLevel = resolvedConfig.proactivity.tasks
268
+ ?? resolvedConfig.proactivity.base
269
+ ?? getProactivityLevelForTask(getPhrenPath());
264
270
  const taskLifecycle = handleTaskPromptLifecycle({
265
271
  phrenPath: getPhrenPath(),
266
272
  prompt,
@@ -275,8 +281,7 @@ export async function handleHookPrompt() {
275
281
  }
276
282
  // Inject finding sensitivity agent instruction
277
283
  try {
278
- const workflowPolicy = getWorkflowPolicy(getPhrenPath());
279
- const sensitivity = workflowPolicy.findingSensitivity ?? "balanced";
284
+ const sensitivity = resolvedConfig.findingSensitivity ?? "balanced";
280
285
  const sensitivityConfig = FINDING_SENSITIVITY_CONFIG[sensitivity];
281
286
  if (sensitivityConfig) {
282
287
  parts.push("");
@@ -304,36 +309,38 @@ export async function handleHookPrompt() {
304
309
  const noticeFile = sessionId ? sessionMarker(getPhrenPath(), `noticed-${sessionId}`) : null;
305
310
  const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
306
311
  if (!alreadyNoticed) {
307
- // Clean up stale session markers (>24h old) from .sessions/ dir
308
- try {
309
- const cutoff = Date.now() - 86400000;
310
- const sessDir = sessionsDir(getPhrenPath());
311
- if (fs.existsSync(sessDir)) {
312
- for (const f of fs.readdirSync(sessDir)) {
313
- if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
312
+ // Defer stale session marker cleanup to after output — it's I/O-heavy and not needed for results
313
+ setImmediate(() => {
314
+ try {
315
+ const cutoff = Date.now() - 86400000;
316
+ const sessDir = sessionsDir(getPhrenPath());
317
+ if (fs.existsSync(sessDir)) {
318
+ for (const f of fs.readdirSync(sessDir)) {
319
+ if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
320
+ continue;
321
+ const fp = `${sessDir}/${f}`;
322
+ try {
323
+ if (fs.statSync(fp).mtimeMs < cutoff)
324
+ fs.unlinkSync(fp);
325
+ }
326
+ catch { /* ignore per-file errors */ }
327
+ }
328
+ }
329
+ // Also clean stale markers from the phren root
330
+ for (const f of fs.readdirSync(getPhrenPath())) {
331
+ if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
314
332
  continue;
315
- const fp = `${sessDir}/${f}`;
316
- if (fs.statSync(fp).mtimeMs < cutoff)
333
+ const fp = `${getPhrenPath()}/${f}`;
334
+ try {
317
335
  fs.unlinkSync(fp);
336
+ }
337
+ catch { /* ignore */ }
318
338
  }
319
339
  }
320
- // Also clean stale markers from the phren root
321
- for (const f of fs.readdirSync(getPhrenPath())) {
322
- if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
323
- continue;
324
- const fp = `${getPhrenPath()}/${f}`;
325
- try {
326
- fs.unlinkSync(fp);
327
- }
328
- catch (err) {
329
- if (process.env.PHREN_DEBUG)
330
- process.stderr.write(`[phren] hookPrompt staleNoticeUnlink: ${errorMessage(err)}\n`);
331
- }
340
+ catch (err) {
341
+ debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
332
342
  }
333
- }
334
- catch (err) {
335
- debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
336
- }
343
+ });
337
344
  const needed = checkConsolidationNeeded(getPhrenPath(), profile);
338
345
  if (needed.length > 0) {
339
346
  const notices = needed.map((n) => {
@@ -345,7 +352,7 @@ export async function handleHookPrompt() {
345
352
  parts.push(`Findings ready for consolidation:`);
346
353
  parts.push(notices.join("\n"));
347
354
  parts.push(`Run phren-consolidate when ready.`);
348
- parts.push(`</phren-notice>`);
355
+ parts.push(`<phren-notice>`);
349
356
  }
350
357
  if (noticeFile) {
351
358
  try {
@@ -367,7 +374,7 @@ export async function handleHookPrompt() {
367
374
  }
368
375
  catch (err) {
369
376
  const msg = errorMessage(err);
370
- process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details.</phren-error>\n`);
377
+ process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details.<phren-error>\n`);
371
378
  debugLog(`hook-prompt error: ${msg}`);
372
379
  process.exit(0);
373
380
  }
@@ -5,6 +5,7 @@ import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjec
5
5
  import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
7
7
  import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
8
+ import { detectSkillCollisions } from "./link-skills.js";
8
9
  import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
9
10
  import { findProjectDir } from "./project-locator.js";
10
11
  import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
@@ -76,7 +77,7 @@ function openInEditor(filePath) {
76
77
  execFileSync(editor, [filePath], { stdio: "inherit" });
77
78
  }
78
79
  catch (err) {
79
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
80
+ if ((process.env.PHREN_DEBUG))
80
81
  process.stderr.write(`[phren] openInEditor: ${errorMessage(err)}\n`);
81
82
  console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
82
83
  process.exit(1);
@@ -145,7 +146,7 @@ export function handleSkillsNamespace(args, profile) {
145
146
  console.log(`Linked skill ${fileName} into ${project}.`);
146
147
  }
147
148
  catch (err) {
148
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
149
+ if ((process.env.PHREN_DEBUG))
149
150
  process.stderr.write(`[phren] skill add symlinkFailed: ${errorMessage(err)}\n`);
150
151
  fs.copyFileSync(source, dest);
151
152
  console.log(`Copied skill ${fileName} into ${project}.`);
@@ -450,7 +451,7 @@ export function handleDetectSkills(args, profile) {
450
451
  continue;
451
452
  }
452
453
  catch (err) {
453
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
454
+ if ((process.env.PHREN_DEBUG))
454
455
  process.stderr.write(`[phren] skillList lstat: ${errorMessage(err)}\n`);
455
456
  }
456
457
  const name = entry.replace(/\.md$/, "");
@@ -548,6 +549,15 @@ function printSkillDoctor(scope, manifest, destDir) {
548
549
  problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
549
550
  }
550
551
  }
552
+ // Check for user-owned files blocking phren skill links
553
+ const phrenPath = getPhrenPath();
554
+ const srcDir = scope.toLowerCase() === "global"
555
+ ? path.join(phrenPath, "global", "skills")
556
+ : path.join(phrenPath, scope, "skills");
557
+ const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
558
+ for (const collision of collisions) {
559
+ problems.push(`Skill collision: ${collision.message}`);
560
+ }
551
561
  }
552
562
  if (!manifest.problems.length && !problems.length) {
553
563
  console.log("\nDoctor: no skill pipeline issues detected.");
@@ -853,7 +863,7 @@ function handleProjectsList(profile) {
853
863
  dirFiles = new Set(fs.readdirSync(projectDir));
854
864
  }
855
865
  catch (err) {
856
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
866
+ if ((process.env.PHREN_DEBUG))
857
867
  process.stderr.write(`[phren] projects list readdir: ${errorMessage(err)}\n`);
858
868
  dirFiles = new Set();
859
869
  }
@@ -896,7 +906,7 @@ async function handleProjectsRemove(name, profile) {
896
906
  countFiles(projectDir);
897
907
  }
898
908
  catch (err) {
899
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
909
+ if ((process.env.PHREN_DEBUG))
900
910
  process.stderr.write(`[phren] projects remove countFiles: ${errorMessage(err)}\n`);
901
911
  }
902
912
  const readline = await import("readline");
@@ -34,7 +34,7 @@ export function readSearchHistory(phrenPath) {
34
34
  .map((line) => JSON.parse(line));
35
35
  }
36
36
  catch (err) {
37
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
37
+ if ((process.env.PHREN_DEBUG))
38
38
  process.stderr.write(`[phren] readSearchHistory: ${errorMessage(err)}\n`);
39
39
  return [];
40
40
  }
@@ -253,7 +253,7 @@ export async function runSearch(opts, phrenPath, profile) {
253
253
  logSearchMiss(phrenPath, opts.query, opts.project);
254
254
  }
255
255
  catch (err) {
256
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
256
+ if ((process.env.PHREN_DEBUG))
257
257
  process.stderr.write(`[phren] search logSearchMiss: ${errorMessage(err)}\n`);
258
258
  }
259
259
  }
@@ -107,7 +107,7 @@ function isAlreadyArchived(referenceDir, bullet) {
107
107
  }
108
108
  }
109
109
  catch (err) {
110
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
110
+ if ((process.env.PHREN_DEBUG))
111
111
  process.stderr.write(`[phren] isDuplicateInReference: ${errorMessage(err)}\n`);
112
112
  }
113
113
  return false;
@@ -271,7 +271,7 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
271
271
  fs.unlinkSync(lockFile);
272
272
  }
273
273
  catch (err) {
274
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
274
+ if ((process.env.PHREN_DEBUG))
275
275
  process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
276
276
  }
277
277
  }
@@ -5,7 +5,7 @@ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "./shared.js";
5
5
  import { errorMessage, runGitOrThrow } from "./utils.js";
6
6
  import { findingIdFromLine } from "./finding-impact.js";
7
7
  import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./content-metadata.js";
8
- import { FINDING_TYPE_DECAY, extractFindingType, parseFindingLifecycle } from "./finding-lifecycle.js";
8
+ import { FINDING_TYPE_DECAY, extractFindingType } from "./finding-lifecycle.js";
9
9
  export const FINDING_PROVENANCE_SOURCES = [
10
10
  "human",
11
11
  "agent",
@@ -294,7 +294,6 @@ export function filterTrustedFindingsDetailed(content, opts) {
294
294
  ...(options.decay || {}),
295
295
  };
296
296
  const highImpactFindingIds = options.highImpactFindingIds;
297
- const impactCounts = options.impactCounts;
298
297
  const project = options.project;
299
298
  const lines = content.split("\n");
300
299
  const out = [];
@@ -413,29 +412,20 @@ export function filterTrustedFindingsDetailed(content, opts) {
413
412
  confidence *= 0.9;
414
413
  if (project && highImpactFindingIds?.size) {
415
414
  const findingId = findingIdFromLine(line);
416
- if (highImpactFindingIds.has(findingId)) {
417
- // Get surface count for graduated boost
418
- const surfaceCount = impactCounts?.get(findingId) ?? 3;
419
- // Log-scaled: 3→1.15x, 10→1.28x, 30→1.38x, capped at 1.4x
420
- const boost = Math.min(1.4, 1 + 0.1 * Math.log2(Math.max(3, surfaceCount)));
421
- confidence *= boost;
422
- // Decay resistance: confirmed findings decay 3x slower
423
- if (effectiveDate) {
424
- const realAge = ageDaysForDate(effectiveDate);
425
- if (realAge !== null) {
426
- const slowedAge = Math.floor(realAge / 3);
427
- confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
428
- }
415
+ if (highImpactFindingIds.has(findingId))
416
+ confidence *= 1.15;
417
+ }
418
+ // Confirmed findings decay 3x slower recompute confidence with reduced age
419
+ {
420
+ const findingId = findingIdFromLine(line);
421
+ if (findingId && highImpactFindingIds?.has(findingId) && effectiveDate) {
422
+ const realAge = ageDaysForDate(effectiveDate);
423
+ if (realAge !== null) {
424
+ const slowedAge = Math.floor(realAge / 3);
425
+ confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
429
426
  }
430
427
  }
431
428
  }
432
- const lifecycle = parseFindingLifecycle(line);
433
- if (lifecycle?.status === "superseded")
434
- confidence *= 0.25;
435
- if (lifecycle?.status === "retracted")
436
- confidence *= 0.1;
437
- if (lifecycle?.status === "contradicted")
438
- confidence *= 0.4;
439
429
  confidence = Math.max(0, Math.min(1, confidence));
440
430
  if (confidence < minConfidence) {
441
431
  issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "stale" });
@@ -2,7 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
  import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "./shared.js";
5
- import { isFeatureEnabled, safeProjectPath } from "./utils.js";
5
+ import { isFeatureEnabled, safeProjectPath, errorMessage } from "./utils.js";
6
6
  import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "./phren-core.js";
7
7
  import { isInactiveFindingLine } from "./finding-lifecycle.js";
8
8
  // ── LLM provider abstraction ────────────────────────────────────────────────
@@ -50,8 +50,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
50
50
  }
51
51
  }
52
52
  catch (err) {
53
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
54
- process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
53
+ if ((process.env.PHREN_DEBUG))
54
+ process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
55
55
  }
56
56
  const result = await compute();
57
57
  // Persist result
@@ -61,8 +61,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
61
61
  persistCache(cachePath, cache);
62
62
  }
63
63
  catch (err) {
64
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
65
- process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${err instanceof Error ? err.message : String(err)}\n`);
64
+ if ((process.env.PHREN_DEBUG))
65
+ process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
66
66
  }
67
67
  return result;
68
68
  }
@@ -562,8 +562,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
562
562
  return { name: e.name, mtime: fs.statSync(fp).mtimeMs, fp };
563
563
  }
564
564
  catch (err) {
565
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
566
- process.stderr.write(`[phren] crossProjectScan stat: ${err instanceof Error ? err.message : String(err)}\n`);
565
+ if ((process.env.PHREN_DEBUG))
566
+ process.stderr.write(`[phren] crossProjectScan stat: ${errorMessage(err)}\n`);
567
567
  return null;
568
568
  }
569
569
  })
@@ -577,8 +577,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
577
577
  }
578
578
  }
579
579
  catch (err) {
580
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
581
- process.stderr.write(`[phren] crossProjectScan: ${err instanceof Error ? err.message : String(err)}\n`);
580
+ if ((process.env.PHREN_DEBUG))
581
+ process.stderr.write(`[phren] crossProjectScan: ${errorMessage(err)}\n`);
582
582
  }
583
583
  const annotations = [];
584
584
  const deadline = Date.now() + CONFLICT_CHECK_TOTAL_TIMEOUT_MS;
@@ -8,7 +8,7 @@ import { isValidProjectName, queueFilePath, safeProjectPath, errorMessage } from
8
8
  import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
9
9
  import { parseFindingLifecycle, } from "./finding-lifecycle.js";
10
10
  import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
11
- export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
11
+ export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, removeTasks, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
12
12
  export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
13
13
  export { loadShellState, readRuntimeHealth, resetShellState, saveShellState, } from "./shell-state-store.js";
14
14
  function withSafeLock(filePath, fn) {
@@ -528,6 +528,29 @@ export function removeTask(phrenPath, project, match) {
528
528
  return phrenOk(`Removed task from ${project}: ${item.line}`);
529
529
  });
530
530
  }
531
+ export function removeTasks(phrenPath, project, matches) {
532
+ const bPath = canonicalTaskFilePath(phrenPath, project);
533
+ if (!bPath)
534
+ return phrenErr(`Project name "${project}" is not valid.`, PhrenError.INVALID_PROJECT_NAME);
535
+ return withSafeLock(bPath, () => {
536
+ const parsed = readTasks(phrenPath, project);
537
+ if (!parsed.ok)
538
+ return forwardErr(parsed);
539
+ const removed = [];
540
+ const errors = [];
541
+ for (const match of matches) {
542
+ const found = findItemByMatch(parsed.data, match);
543
+ if (found.error || !found.match) {
544
+ errors.push(match);
545
+ continue;
546
+ }
547
+ const [item] = parsed.data.items[found.match.section].splice(found.match.index, 1);
548
+ removed.push(item.line);
549
+ }
550
+ writeTaskDoc(parsed.data);
551
+ return phrenOk({ removed, errors });
552
+ });
553
+ }
531
554
  export function updateTask(phrenPath, project, match, updates) {
532
555
  const bPath = canonicalTaskFilePath(phrenPath, project);
533
556
  if (!bPath)