@phren/cli 0.0.41 → 0.0.43

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.
@@ -291,7 +291,7 @@ export async function handleUpdate(args) {
291
291
  process.exitCode = 1;
292
292
  }
293
293
  else {
294
- console.log("Run 'npx phren init' to refresh hooks and config.");
294
+ console.log("Run 'phren init' to refresh hooks and config.");
295
295
  }
296
296
  }
297
297
  export async function handleReview(args) {
@@ -7,7 +7,7 @@ import { handleExtractMemories } from "./extract.js";
7
7
  import { handleGovernMemories, handlePruneMemories, handleConsolidateMemories, handleMaintain, handleBackgroundMaintenance, } from "./govern.js";
8
8
  import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowPolicy, } from "./config.js";
9
9
  import { parseSearchArgs } from "./search.js";
10
- import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handleStoreNamespace, handleTaskNamespace, } from "./namespaces.js";
10
+ import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handlePromoteNamespace, handleStoreNamespace, handleTaskNamespace, } from "./namespaces.js";
11
11
  import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInjection, handleInspectIndex, } from "./ops.js";
12
12
  import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./actions.js";
13
13
  import { handleGraphNamespace } from "./graph.js";
@@ -109,6 +109,8 @@ export async function runCliCommand(command, args) {
109
109
  return handleSessionContext();
110
110
  case "store":
111
111
  return handleStoreNamespace(args);
112
+ case "promote":
113
+ return handlePromoteNamespace(args);
112
114
  default:
113
115
  console.error(`Unknown command: ${command}\nRun 'phren --help' for available commands.`);
114
116
  process.exit(1);
@@ -86,13 +86,13 @@ export function getUntrackedProjectNotice(phrenPath, cwd) {
86
86
  return [
87
87
  "<phren-notice>",
88
88
  "This project directory is not tracked by phren yet.",
89
- "Run `npx phren add` to track it now.",
90
- `Suggested command: \`npx phren add \"${projectDir}\"\``,
89
+ "Run `phren add` to track it now.",
90
+ `Suggested command: \`phren add \"${projectDir}\"\``,
91
91
  "Ask the user whether they want to add it to phren now.",
92
- "If they say no, tell them they can always run `npx phren add` later.",
92
+ "If they say no, tell them they can always run `phren add` later.",
93
93
  "If they say yes, also ask whether phren should manage repo instruction files or leave their existing repo-owned CLAUDE/AGENTS files alone.",
94
- `Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`npx phren add\` from that directory.`,
95
- "After onboarding, run `npx phren doctor` if hooks or MCP tools are not responding.",
94
+ `Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`phren add\` from that directory.`,
95
+ "After onboarding, run `phren doctor` if hooks or MCP tools are not responding.",
96
96
  "<phren-notice>",
97
97
  "",
98
98
  ].join("\n");
@@ -127,8 +127,8 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
127
127
  return [
128
128
  "<phren-notice>",
129
129
  "Phren onboarding: no tracked projects are active for this workspace yet.",
130
- "Start in a project repo and run `npx phren add` so SessionStart can inject project context.",
131
- "Run `npx phren doctor` to verify hooks and MCP wiring after setup.",
130
+ "Start in a project repo and run `phren add` so SessionStart can inject project context.",
131
+ "Run `phren doctor` to verify hooks and MCP wiring after setup.",
132
132
  "<phren-notice>",
133
133
  "",
134
134
  ].join("\n");
@@ -141,7 +141,7 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
141
141
  "<phren-notice>",
142
142
  `Phren onboarding: project "${activeProject}" is tracked but memory is still empty.`,
143
143
  "Capture one finding with `add_finding` and one task with `add_task` to seed future SessionStart context.",
144
- "Run `npx phren doctor` if setup seems incomplete.",
144
+ "Run `phren doctor` if setup seems incomplete.",
145
145
  "<phren-notice>",
146
146
  "",
147
147
  ].join("\n");
@@ -594,7 +594,7 @@ export async function handleProjectsNamespace(args, profile) {
594
594
  }
595
595
  if (subcommand === "add") {
596
596
  console.error("`phren projects add` has been removed from the supported workflow.");
597
- console.error("Use `cd ~/your-project && npx phren add` so enrollment stays path-based.");
597
+ console.error("Use `cd ~/your-project && phren add` so enrollment stays path-based.");
598
598
  process.exit(1);
599
599
  }
600
600
  if (subcommand === "remove") {
@@ -864,7 +864,7 @@ function handleProjectsList(profile) {
864
864
  .filter((name) => name !== "global")
865
865
  .sort();
866
866
  if (!projects.length) {
867
- console.log("No projects found. Run: cd ~/your-project && npx phren add");
867
+ console.log("No projects found. Run: cd ~/your-project && phren add");
868
868
  return;
869
869
  }
870
870
  console.log(`\nProjects in ${phrenPath}:\n`);
@@ -888,7 +888,7 @@ function handleProjectsList(profile) {
888
888
  console.log(` ${name}${tagStr}`);
889
889
  }
890
890
  console.log(`\n${projects.length} project(s) total.`);
891
- console.log("Add another project: cd ~/your-project && npx phren add");
891
+ console.log("Add another project: cd ~/your-project && phren add");
892
892
  }
893
893
  async function handleProjectsRemove(name, profile) {
894
894
  if (!isValidProjectName(name)) {
@@ -1509,6 +1509,7 @@ function printStoreUsage() {
1509
1509
  console.log(" phren store add <name> --remote <url> Add a team store");
1510
1510
  console.log(" phren store remove <name> Remove a store (local only)");
1511
1511
  console.log(" phren store sync Pull all stores");
1512
+ console.log(" phren store activity [--limit N] Recent team findings");
1512
1513
  }
1513
1514
  export async function handleStoreNamespace(args) {
1514
1515
  const subcommand = args[0];
@@ -1630,6 +1631,47 @@ export async function handleStoreNamespace(args) {
1630
1631
  }
1631
1632
  return;
1632
1633
  }
1634
+ if (subcommand === "activity") {
1635
+ const stores = resolveAllStores(phrenPath);
1636
+ const teamStores = stores.filter((s) => s.role === "team");
1637
+ if (teamStores.length === 0) {
1638
+ console.log("No team stores registered. Add one with: phren store add <name> --remote <url>");
1639
+ return;
1640
+ }
1641
+ const { readTeamJournalEntries } = await import("../finding/journal.js");
1642
+ const limit = Number(getOptionValue(args.slice(1), "--limit") ?? "20");
1643
+ const allEntries = [];
1644
+ for (const store of teamStores) {
1645
+ if (!fs.existsSync(store.path))
1646
+ continue;
1647
+ const projectDirs = getProjectDirs(store.path);
1648
+ for (const dir of projectDirs) {
1649
+ const projectName = path.basename(dir);
1650
+ const journalEntries = readTeamJournalEntries(store.path, projectName);
1651
+ for (const je of journalEntries) {
1652
+ for (const entry of je.entries) {
1653
+ allEntries.push({ store: store.name, project: projectName, date: je.date, actor: je.actor, entry });
1654
+ }
1655
+ }
1656
+ }
1657
+ }
1658
+ allEntries.sort((a, b) => b.date.localeCompare(a.date));
1659
+ const capped = allEntries.slice(0, limit);
1660
+ if (capped.length === 0) {
1661
+ console.log("No team activity yet.");
1662
+ return;
1663
+ }
1664
+ console.log(`Team activity (${capped.length}/${allEntries.length}):\n`);
1665
+ let lastDate = "";
1666
+ for (const e of capped) {
1667
+ if (e.date !== lastDate) {
1668
+ console.log(`## ${e.date}`);
1669
+ lastDate = e.date;
1670
+ }
1671
+ console.log(` [${e.store}/${e.project}] ${e.actor}: ${e.entry}`);
1672
+ }
1673
+ return;
1674
+ }
1633
1675
  if (subcommand === "sync") {
1634
1676
  const stores = resolveAllStores(phrenPath);
1635
1677
  let hasErrors = false;
@@ -1665,6 +1707,71 @@ export async function handleStoreNamespace(args) {
1665
1707
  printStoreUsage();
1666
1708
  process.exit(1);
1667
1709
  }
1710
+ // ── Promote namespace ────────────────────────────────────────────────────────
1711
+ export async function handlePromoteNamespace(args) {
1712
+ if (!args[0] || args[0] === "--help" || args[0] === "-h") {
1713
+ console.log("Usage:");
1714
+ console.log(' phren promote <project> "finding text..." --to <store>');
1715
+ console.log(" Copies a finding from the primary store to a team store.");
1716
+ return;
1717
+ }
1718
+ const phrenPath = getPhrenPath();
1719
+ const project = args[0];
1720
+ if (!isValidProjectName(project)) {
1721
+ console.error(`Invalid project name: "${project}"`);
1722
+ process.exit(1);
1723
+ }
1724
+ const toStore = getOptionValue(args.slice(1), "--to");
1725
+ if (!toStore) {
1726
+ console.error("--to <store> is required. Specify the target team store.");
1727
+ process.exit(1);
1728
+ }
1729
+ // Everything between project and --to is the finding text
1730
+ const toIdx = args.indexOf("--to");
1731
+ const findingText = args.slice(1, toIdx !== -1 ? toIdx : undefined).join(" ").trim();
1732
+ if (!findingText) {
1733
+ console.error("Finding text is required.");
1734
+ process.exit(1);
1735
+ }
1736
+ const stores = resolveAllStores(phrenPath);
1737
+ const targetStore = stores.find((s) => s.name === toStore);
1738
+ if (!targetStore) {
1739
+ const available = stores.map((s) => s.name).join(", ");
1740
+ console.error(`Store "${toStore}" not found. Available: ${available}`);
1741
+ process.exit(1);
1742
+ }
1743
+ if (targetStore.role === "readonly") {
1744
+ console.error(`Store "${toStore}" is read-only.`);
1745
+ process.exit(1);
1746
+ }
1747
+ if (targetStore.role === "primary") {
1748
+ console.error(`Cannot promote to primary store — finding is already there.`);
1749
+ process.exit(1);
1750
+ }
1751
+ // Find the matching finding in the primary store
1752
+ const { readFindings } = await import("../data/access.js");
1753
+ const findingsResult = readFindings(phrenPath, project);
1754
+ if (!findingsResult.ok) {
1755
+ console.error(`Could not read findings for project "${project}".`);
1756
+ process.exit(1);
1757
+ }
1758
+ const match = findingsResult.data.find((item) => item.text.includes(findingText) || findingText.includes(item.text));
1759
+ if (!match) {
1760
+ console.error(`No finding matching "${findingText.slice(0, 80)}..." found in ${project}.`);
1761
+ process.exit(1);
1762
+ }
1763
+ // Write to target store
1764
+ const targetProjectDir = path.join(targetStore.path, project);
1765
+ fs.mkdirSync(targetProjectDir, { recursive: true });
1766
+ const { addFindingToFile } = await import("../shared/content.js");
1767
+ const result = addFindingToFile(targetStore.path, project, match.text);
1768
+ if (!result.ok) {
1769
+ console.error(`Failed to add finding to ${toStore}: ${result.error}`);
1770
+ process.exit(1);
1771
+ }
1772
+ console.log(`Promoted to ${toStore}/${project}:`);
1773
+ console.log(` "${match.text.slice(0, 120)}${match.text.length > 120 ? "..." : ""}"`);
1774
+ }
1668
1775
  function countStoreProjects(store) {
1669
1776
  if (!fs.existsSync(store.path))
1670
1777
  return 0;
@@ -60,13 +60,13 @@ export function getUntrackedProjectNotice(phrenPath, cwd) {
60
60
  return [
61
61
  "<phren-notice>",
62
62
  "This project directory is not tracked by phren yet.",
63
- "Run `npx phren add` to track it now.",
64
- `Suggested command: \`npx phren add "${projectDir}"\``,
63
+ "Run `phren add` to track it now.",
64
+ `Suggested command: \`phren add "${projectDir}"\``,
65
65
  "Ask the user whether they want to add it to phren now.",
66
- "If they say no, tell them they can always run `npx phren add` later.",
66
+ "If they say no, tell them they can always run `phren add` later.",
67
67
  "If they say yes, also ask whether phren should manage repo instruction files or leave their existing repo-owned CLAUDE/AGENTS files alone.",
68
- `Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`npx phren add\` from that directory.`,
69
- "After onboarding, run `npx phren doctor` if hooks or MCP tools are not responding.",
68
+ `Then use the \`add_project\` MCP tool with path="${projectDir}" and ownership="phren-managed"|"detached"|"repo-managed", or run \`phren add\` from that directory.`,
69
+ "After onboarding, run `phren doctor` if hooks or MCP tools are not responding.",
70
70
  "<phren-notice>",
71
71
  "",
72
72
  ].join("\n");
@@ -83,8 +83,8 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
83
83
  return [
84
84
  "<phren-notice>",
85
85
  "Phren onboarding: no tracked projects are active for this workspace yet.",
86
- "Start in a project repo and run `npx phren add` so SessionStart can inject project context.",
87
- "Run `npx phren doctor` to verify hooks and MCP wiring after setup.",
86
+ "Start in a project repo and run `phren add` so SessionStart can inject project context.",
87
+ "Run `phren doctor` to verify hooks and MCP wiring after setup.",
88
88
  "<phren-notice>",
89
89
  "",
90
90
  ].join("\n");
@@ -97,7 +97,7 @@ export function getSessionStartOnboardingNotice(phrenPath, cwd, activeProject) {
97
97
  "<phren-notice>",
98
98
  `Phren onboarding: project "${activeProject}" is tracked but memory is still empty.`,
99
99
  "Capture one finding with `add_finding` and one task with `add_task` to seed future SessionStart context.",
100
- "Run `npx phren doctor` if setup seems incomplete.",
100
+ "Run `phren doctor` if setup seems incomplete.",
101
101
  "<phren-notice>",
102
102
  "",
103
103
  ].join("\n");
@@ -338,12 +338,15 @@ export function isDuplicateFinding(existingContent, newLearning, threshold = 0.6
338
338
  return true;
339
339
  }
340
340
  // Second pass: Jaccard similarity (strip metadata before comparing)
341
+ // Threshold lowered from 0.55 to 0.40 to catch agent paraphrases —
342
+ // swarm agents often report the same insight with different wording,
343
+ // and 0.55 let too many through.
341
344
  const newTokens = jaccardTokenize(stripMetadata(newLearning));
342
345
  const existingTokens = jaccardTokenize(stripMetadata(bullet));
343
346
  if (newTokens.size < 3 || existingTokens.size < 3)
344
347
  continue; // too few tokens for reliable Jaccard
345
348
  const jaccard = jaccardSimilarity(newTokens, existingTokens);
346
- if (jaccard > 0.55) {
349
+ if (jaccard > 0.40) {
347
350
  debugLog(`duplicate-detection: Jaccard ${Math.round(jaccard * 100)}% with existing: "${bullet.slice(0, 80)}"`);
348
351
  return true;
349
352
  }
@@ -489,7 +492,7 @@ export async function checkSemanticDedup(phrenPath, project, newLearning, signal
489
492
  if (tokA.size < 3 || tokB.size < 3)
490
493
  continue;
491
494
  const jaccard = jaccardSimilarity(tokA, tokB);
492
- if (jaccard >= 0.55)
495
+ if (jaccard >= 0.40)
493
496
  continue; // already caught by sync isDuplicateFinding
494
497
  if (jaccard >= 0.3) {
495
498
  const isDup = await semanticDedup(a, b, phrenPath, signal);
@@ -175,6 +175,7 @@ const CLI_COMMANDS = [
175
175
  "consolidation-status",
176
176
  "session-context",
177
177
  "store",
178
+ "promote",
178
179
  ];
179
180
  async function flushTopLevelOutput() {
180
181
  await Promise.all([
@@ -264,7 +265,7 @@ export async function runTopLevelCommand(argv) {
264
265
  const phrenPath = defaultPhrenPath();
265
266
  const profile = (process.env.PHREN_PROFILE) || undefined;
266
267
  if (!fs.existsSync(phrenPath) || !fs.existsSync(path.join(phrenPath, ".config"))) {
267
- console.log("phren is not set up yet. Run: npx phren init");
268
+ console.log("phren is not set up yet. Run: phren init");
268
269
  return finish(1);
269
270
  }
270
271
  const ownership = ownershipArg
@@ -380,7 +381,7 @@ export async function runTopLevelCommand(argv) {
380
381
  const note = getVerifyOutcomeNote(phrenPath, result.checks);
381
382
  if (note)
382
383
  console.log(`\nNote: ${note}`);
383
- console.log(`\nRun \`npx phren init\` to fix setup issues.`);
384
+ console.log(`\nRun \`phren init\` to fix setup issues.`);
384
385
  }
385
386
  return finish(result.ok ? 0 : 1);
386
387
  }
@@ -407,7 +408,7 @@ export async function runTopLevelCommand(argv) {
407
408
  }
408
409
  }
409
410
  if (argvCommand === "link") {
410
- console.error("`phren link` has been removed. Use `npx phren init` instead.");
411
+ console.error("`phren link` has been removed. Use `phren init` instead.");
411
412
  return finish(1);
412
413
  }
413
414
  if (argvCommand === "--health") {
@@ -167,6 +167,7 @@ export function resolveFindingSessionId(phrenPath, project, explicitSessionId) {
167
167
  .sort((a, b) => sessionSortValue(b) - sessionSortValue(a));
168
168
  if (matchingProject.length > 0)
169
169
  return matchingProject[0].sessionId;
170
- const sorted = active.sort((a, b) => sessionSortValue(b) - sessionSortValue(a));
171
- return sorted[0]?.sessionId;
170
+ // Do not attribute findings to an unrelated active session from another project.
171
+ // Missing session provenance is safer than cross-project contamination.
172
+ return undefined;
172
173
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import * as crypto from "crypto";
4
- import { runtimeDir, phrenOk, phrenErr, PhrenError } from "../shared.js";
4
+ import { runtimeDir, phrenOk, phrenErr, PhrenError, atomicWriteText } from "../shared.js";
5
5
  import { withFileLock } from "../shared/governance.js";
6
6
  import { addFindingToFile } from "../shared/content.js";
7
7
  import { isValidProjectName, errorMessage } from "../utils.js";
@@ -120,3 +120,90 @@ export function compactFindingJournals(phrenPath, project) {
120
120
  }
121
121
  return result;
122
122
  }
123
+ // ── Team store journal (append-only markdown, committed to git) ──────────────
124
+ const TEAM_JOURNAL_DIR = "journal";
125
+ /**
126
+ * Append a finding to a team store's journal.
127
+ * Each actor gets one file per day — no merge conflicts possible.
128
+ * These are markdown files committed to git (not runtime JSONL).
129
+ */
130
+ export function appendTeamJournal(storePath, project, finding, actor) {
131
+ const resolvedActor = actor || process.env.PHREN_ACTOR || process.env.USER || "unknown";
132
+ const date = new Date().toISOString().slice(0, 10);
133
+ const journalDir = path.join(storePath, project, TEAM_JOURNAL_DIR);
134
+ const journalFile = `${date}-${resolvedActor}.md`;
135
+ const journalPath = path.join(journalDir, journalFile);
136
+ try {
137
+ fs.mkdirSync(journalDir, { recursive: true });
138
+ const entry = `- ${finding}\n`;
139
+ if (fs.existsSync(journalPath)) {
140
+ fs.appendFileSync(journalPath, entry);
141
+ }
142
+ else {
143
+ fs.writeFileSync(journalPath, `## ${date} (${resolvedActor})\n\n${entry}`);
144
+ }
145
+ return phrenOk(journalFile);
146
+ }
147
+ catch (err) {
148
+ return phrenErr(`Team journal append failed: ${errorMessage(err)}`, PhrenError.PERMISSION_DENIED);
149
+ }
150
+ }
151
+ /**
152
+ * Read all team journal entries for a project, newest first.
153
+ */
154
+ export function readTeamJournalEntries(storePath, project) {
155
+ const journalDir = path.join(storePath, project, TEAM_JOURNAL_DIR);
156
+ if (!fs.existsSync(journalDir))
157
+ return [];
158
+ return fs.readdirSync(journalDir)
159
+ .filter((f) => f.endsWith(".md"))
160
+ .sort()
161
+ .reverse()
162
+ .map((file) => {
163
+ const match = file.match(/^(\d{4}-\d{2}-\d{2})-(.+)\.md$/);
164
+ const date = match?.[1] ?? "unknown";
165
+ const actor = match?.[2] ?? "unknown";
166
+ const content = fs.readFileSync(path.join(journalDir, file), "utf8");
167
+ const entries = content.split("\n")
168
+ .filter((line) => line.startsWith("- "))
169
+ .map((line) => line.slice(2).trim());
170
+ return { file, date, actor, entries };
171
+ });
172
+ }
173
+ /**
174
+ * Materialize FINDINGS.md from team journal entries.
175
+ * Groups by date, includes actor attribution.
176
+ */
177
+ export function materializeTeamFindings(storePath, project) {
178
+ const journalEntries = readTeamJournalEntries(storePath, project);
179
+ if (journalEntries.length === 0) {
180
+ return phrenErr("No journal entries found", PhrenError.FILE_NOT_FOUND);
181
+ }
182
+ // Group by date, chronological order
183
+ const byDate = new Map();
184
+ for (const entry of [...journalEntries].reverse()) {
185
+ if (!byDate.has(entry.date))
186
+ byDate.set(entry.date, []);
187
+ byDate.get(entry.date).push({ actor: entry.actor, entries: entry.entries });
188
+ }
189
+ const lines = [`# ${project} findings\n`];
190
+ let count = 0;
191
+ for (const [date, actors] of byDate) {
192
+ lines.push(`## ${date}`);
193
+ for (const { actor, entries } of actors) {
194
+ for (const entry of entries) {
195
+ lines.push(`- ${entry} <!-- author:${actor} -->`);
196
+ count++;
197
+ }
198
+ }
199
+ lines.push("");
200
+ }
201
+ const findingsPath = path.join(storePath, project, "FINDINGS.md");
202
+ try {
203
+ atomicWriteText(findingsPath, lines.join("\n"));
204
+ return phrenOk({ entryCount: count });
205
+ }
206
+ catch (err) {
207
+ return phrenErr(`Materialize failed: ${errorMessage(err)}`, PhrenError.PERMISSION_DENIED);
208
+ }
209
+ }
@@ -207,7 +207,7 @@ export function configureClaude(phrenPath, opts = {}) {
207
207
  eventHooks.push({ matcher: "", hooks: [hookBody] });
208
208
  }
209
209
  };
210
- const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false);
210
+ const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", true);
211
211
  if (hooksEnabled) {
212
212
  upsertPhrenHook("UserPromptSubmit", {
213
213
  type: "command",
@@ -89,7 +89,7 @@ export function configureHooksIfEnabled(phrenPath, hooksEnabled, verb) {
89
89
  }
90
90
  }
91
91
  else {
92
- log(` Hooks are disabled by preference (run: npx phren hooks-mode on)`);
92
+ log(` Hooks are disabled by preference (run: phren hooks-mode on)`);
93
93
  }
94
94
  // Install phren CLI wrapper at ~/.local/bin/phren so the bare command works
95
95
  const wrapperInstalled = installPhrenCliWrapper(phrenPath);
@@ -14,7 +14,7 @@ export async function runHooksMode(modeArg) {
14
14
  if (!normalizedArg || normalizedArg === "status") {
15
15
  const current = getHooksEnabledPreference(phrenPath);
16
16
  log(`Hooks mode: ${current ? "on (active)" : "off (disabled)"}`);
17
- log(`Change mode: npx phren hooks-mode on|off`);
17
+ log(`Change mode: phren hooks-mode on|off`);
18
18
  return;
19
19
  }
20
20
  const mode = parseMcpMode(normalizedArg);
@@ -15,8 +15,8 @@ export async function runMcpMode(modeArg) {
15
15
  const hooks = getHooksEnabledPreference(phrenPath);
16
16
  log(`MCP mode: ${current ? "on (recommended)" : "off (hooks-only fallback)"}`);
17
17
  log(`Hooks mode: ${hooks ? "on (active)" : "off (disabled)"}`);
18
- log(`Change mode: npx phren mcp-mode on|off`);
19
- log(`Hooks toggle: npx phren hooks-mode on|off`);
18
+ log(`Change mode: phren mcp-mode on|off`);
19
+ log(`Hooks toggle: phren hooks-mode on|off`);
20
20
  return;
21
21
  }
22
22
  const mode = parseMcpMode(normalizedArg);
@@ -280,7 +280,7 @@ export async function runWalkthrough(phrenPath, options) {
280
280
  log(" phren-managed: Phren may mirror CLAUDE.md / AGENTS.md into the repo");
281
281
  log(" detached: Phren keeps its own docs but does not write into the repo");
282
282
  log(" repo-managed: keep the repo's existing CLAUDE/AGENTS files as canonical");
283
- log(" Change later: npx phren config project-ownership <mode>");
283
+ log(" Change later: phren config project-ownership <mode>");
284
284
  const projectOwnershipDefault = await prompts.select("Default project ownership", [
285
285
  { value: "detached", name: "detached (default)" },
286
286
  { value: "phren-managed", name: "phren-managed" },
@@ -291,7 +291,7 @@ export async function runWalkthrough(phrenPath, options) {
291
291
  log("directly: search memory, manage tasks, save findings, etc.");
292
292
  log(" Recommended for: Claude Code, Cursor, Copilot CLI, Codex");
293
293
  log(" Alternative: hooks-only mode (read-only context injection, any agent)");
294
- log(" Change later: npx phren mcp-mode on|off");
294
+ log(" Change later: phren mcp-mode on|off");
295
295
  const mcp = (await prompts.confirm("Enable MCP?", true)) ? "on" : "off";
296
296
  printSection("Hooks");
297
297
  log("Hooks run shell commands at session start, prompt submit, and session end.");
@@ -299,7 +299,7 @@ export async function runWalkthrough(phrenPath, options) {
299
299
  log(" - UserPromptSubmit: searches phren and injects relevant context");
300
300
  log(" - Stop: commits and pushes any new findings after each response");
301
301
  log(" What they touch: ~/.claude/settings.json (hooks section only)");
302
- log(" Change later: npx phren hooks-mode on|off");
302
+ log(" Change later: phren hooks-mode on|off");
303
303
  const hooks = (await prompts.confirm("Enable hooks?", true)) ? "on" : "off";
304
304
  printSection("Semantic Search (Optional)");
305
305
  log("Phren can use a local embedding model for semantic (fuzzy) search via Ollama.");
@@ -347,7 +347,7 @@ export async function runWalkthrough(phrenPath, options) {
347
347
  let findingsProactivity = "high";
348
348
  if (autoCaptureEnabled) {
349
349
  log(" Findings capture level controls how eager phren is to save lessons automatically.");
350
- log(" Change later: npx phren config proactivity.findings <high|medium|low>");
350
+ log(" Change later: phren config proactivity.findings <high|medium|low>");
351
351
  findingsProactivity = await prompts.select("Findings capture level", [
352
352
  { value: "high", name: "high (recommended)" },
353
353
  { value: "medium", name: "medium" },
@@ -363,7 +363,7 @@ export async function runWalkthrough(phrenPath, options) {
363
363
  log(" suggest: proposes tasks but waits for approval before writing");
364
364
  log(" manual: tasks are fully manual — you add them yourself");
365
365
  log(" off: never touch tasks automatically");
366
- log(" Change later: npx phren config workflow set --taskMode=<mode>");
366
+ log(" Change later: phren config workflow set --taskMode=<mode>");
367
367
  const taskMode = await prompts.select("Task mode", [
368
368
  { value: "auto", name: "auto (recommended)" },
369
369
  { value: "suggest", name: "suggest" },
@@ -376,7 +376,7 @@ export async function runWalkthrough(phrenPath, options) {
376
376
  log(" high (recommended): captures tasks as they come up naturally");
377
377
  log(" medium: only when you explicitly mention a task");
378
378
  log(" low: minimal auto-capture");
379
- log(" Change later: npx phren config proactivity.tasks <high|medium|low>");
379
+ log(" Change later: phren config proactivity.tasks <high|medium|low>");
380
380
  taskProactivity = await prompts.select("Task proactivity", [
381
381
  { value: "high", name: "high (recommended)" },
382
382
  { value: "medium", name: "medium" },
@@ -387,7 +387,7 @@ export async function runWalkthrough(phrenPath, options) {
387
387
  log("Choose how strict review gates should be for risky or low-confidence writes.");
388
388
  log(" lowConfidenceThreshold: confidence cutoff used to mark writes as risky");
389
389
  log(" riskySections: sections always treated as risky");
390
- log(" Change later: npx phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
390
+ log(" Change later: phren config workflow set --lowConfidenceThreshold=0.7 --riskySections=Stale,Conflicts");
391
391
  const thresholdAnswer = await prompts.input("Low-confidence threshold [0.0-1.0]", "0.7");
392
392
  const lowConfidenceThreshold = parseLowConfidenceThreshold(thresholdAnswer, 0.7);
393
393
  const riskySectionsAnswer = await prompts.input("Risky sections [Review,Stale,Conflicts]", "Stale,Conflicts");
@@ -437,7 +437,7 @@ export async function runWalkthrough(phrenPath, options) {
437
437
  log(" conservative — decisions and pitfalls only");
438
438
  log(" balanced — non-obvious patterns, decisions, pitfalls, bugs (recommended)");
439
439
  log(" aggressive — everything worth remembering, err on the side of capturing");
440
- log(" Change later: npx phren config finding-sensitivity <level>");
440
+ log(" Change later: phren config finding-sensitivity <level>");
441
441
  const findingSensitivity = await prompts.select("Finding sensitivity", [
442
442
  { value: "balanced", name: "balanced (recommended)" },
443
443
  { value: "conservative", name: "conservative" },
@@ -466,7 +466,7 @@ export async function runWalkthrough(phrenPath, options) {
466
466
  bootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
467
467
  if (!bootstrapCurrentProject) {
468
468
  bootstrapCurrentProject = false;
469
- log(style.warning(` Skipped. Later: cd ${detectedProject} && npx phren add`));
469
+ log(style.warning(` Skipped. Later: cd ${detectedProject} && phren add`));
470
470
  }
471
471
  else {
472
472
  bootstrapOwnership = await prompts.select("Ownership for detected project", [
@@ -260,7 +260,7 @@ export async function runInit(opts = {}) {
260
260
  shouldBootstrapCurrentProject = await prompts.confirm("Add this project to phren now?", true);
261
261
  if (!shouldBootstrapCurrentProject) {
262
262
  shouldBootstrapCurrentProject = false;
263
- log(style.warning(` Skipped. Later: cd ${pendingBootstrap.path} && npx phren add`));
263
+ log(style.warning(` Skipped. Later: cd ${pendingBootstrap.path} && phren add`));
264
264
  }
265
265
  else {
266
266
  bootstrapOwnership = await prompts.select("Ownership for detected project", [
@@ -352,7 +352,7 @@ export async function runInit(opts = {}) {
352
352
  const previousVersion = prefs.installedVersion;
353
353
  if (isVersionNewer(VERSION, previousVersion)) {
354
354
  log(`\n Starter template update available: v${previousVersion} -> v${VERSION}`);
355
- log(` Run \`npx phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
355
+ log(` Run \`phren init --apply-starter-update\` to refresh global/CLAUDE.md and global skills.`);
356
356
  }
357
357
  if (opts.applyStarterUpdate) {
358
358
  const updated = applyStarterTemplateUpdates(phrenPath);
@@ -395,8 +395,8 @@ export async function runInit(opts = {}) {
395
395
  log(`\n\x1b[95m◆\x1b[0m phren updated successfully`);
396
396
  log(`\nNext steps:`);
397
397
  log(` 1. Start a new Claude session in your project directory — phren injects context automatically`);
398
- log(` 2. Run \`npx phren doctor\` to verify everything is wired correctly`);
399
- log(` 3. Change defaults anytime: \`npx phren config project-ownership\`, \`npx phren config workflow\`, \`npx phren config proactivity.findings\`, \`npx phren config proactivity.tasks\``);
398
+ log(` 2. Run \`phren doctor\` to verify everything is wired correctly`);
399
+ log(` 3. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
400
400
  log(` 4. After your first week, run phren-discover to surface gaps in your project knowledge`);
401
401
  log(` 5. After working across projects, run phren-consolidate to find cross-project patterns`);
402
402
  log(``);
@@ -581,8 +581,8 @@ export async function runInit(opts = {}) {
581
581
  log(`\nNext steps:`);
582
582
  let step = 1;
583
583
  log(` ${step++}. Start a new Claude session in your project directory — phren injects context automatically`);
584
- log(` ${step++}. Run \`npx phren doctor\` to verify everything is wired correctly`);
585
- log(` ${step++}. Change defaults anytime: \`npx phren config project-ownership\`, \`npx phren config workflow\`, \`npx phren config proactivity.findings\`, \`npx phren config proactivity.tasks\``);
584
+ log(` ${step++}. Run \`phren doctor\` to verify everything is wired correctly`);
585
+ log(` ${step++}. Change defaults anytime: \`phren config project-ownership\`, \`phren config workflow\`, \`phren config proactivity.findings\`, \`phren config proactivity.tasks\``);
586
586
  const gh = opts._walkthroughGithub;
587
587
  if (gh) {
588
588
  const remote = gh.username
@@ -607,9 +607,9 @@ export async function runInit(opts = {}) {
607
607
  log(` git remote add origin git@github.com:YOUR_USERNAME/my-phren.git`);
608
608
  log(` git push -u origin main`);
609
609
  }
610
- log(` ${step++}. Add more projects: cd ~/your-project && npx phren add`);
610
+ log(` ${step++}. Add more projects: cd ~/your-project && phren add`);
611
611
  if (!mcpEnabled) {
612
- log(` ${step++}. Turn MCP on: npx phren mcp-mode on`);
612
+ log(` ${step++}. Turn MCP on: phren mcp-mode on`);
613
613
  }
614
614
  log(` ${step++}. After your first week, run phren-discover to surface gaps in your project knowledge`);
615
615
  log(` ${step++}. After working across projects, run phren-consolidate to find cross-project patterns`);
@@ -1089,9 +1089,15 @@ export function updateMachinesYaml(phrenPath, machine, profile) {
1089
1089
  */
1090
1090
  export function detectProjectDir(dir, phrenPath) {
1091
1091
  const home = os.homedir();
1092
+ const tmpRoot = path.resolve(os.tmpdir());
1092
1093
  const resolvedPhrenPath = path.resolve(phrenPath);
1093
1094
  let current = path.resolve(dir);
1094
1095
  while (true) {
1096
+ // Never treat the shared OS temp root itself as a project. Tools may drop
1097
+ // global instruction files there, which would otherwise hijack detection
1098
+ // for arbitrary temp subdirectories.
1099
+ if (current === tmpRoot)
1100
+ return null;
1095
1101
  if (current === home || current === resolvedPhrenPath)
1096
1102
  return null;
1097
1103
  if (current.startsWith(resolvedPhrenPath + path.sep))