@phren/cli 0.0.42 → 0.0.44

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 (41) hide show
  1. package/mcp/dist/cli/actions.js +24 -1
  2. package/mcp/dist/cli/cli.js +6 -1
  3. package/mcp/dist/cli/hooks-session.js +8 -8
  4. package/mcp/dist/cli/namespaces.js +3 -4
  5. package/mcp/dist/cli/team.js +301 -0
  6. package/mcp/dist/cli-hooks-session-handlers.js +26 -8
  7. package/mcp/dist/cli-hooks-stop.js +35 -5
  8. package/mcp/dist/content/dedup.js +5 -2
  9. package/mcp/dist/entrypoint.js +11 -3
  10. package/mcp/dist/finding/context.js +3 -2
  11. package/mcp/dist/init/config.js +1 -1
  12. package/mcp/dist/init/init-configure.js +1 -1
  13. package/mcp/dist/init/init-hooks-mode.js +1 -1
  14. package/mcp/dist/init/init-mcp-mode.js +2 -2
  15. package/mcp/dist/init/init-walkthrough.js +9 -9
  16. package/mcp/dist/init/init.js +8 -8
  17. package/mcp/dist/init/setup.js +46 -1
  18. package/mcp/dist/init-fresh.js +4 -4
  19. package/mcp/dist/init-hooks.js +1 -1
  20. package/mcp/dist/init-modes.js +3 -3
  21. package/mcp/dist/init-update.js +3 -3
  22. package/mcp/dist/init-walkthrough.js +9 -9
  23. package/mcp/dist/link/doctor.js +1 -1
  24. package/mcp/dist/link/link.js +1 -1
  25. package/mcp/dist/phren-paths.js +2 -2
  26. package/mcp/dist/profile-store.js +2 -2
  27. package/mcp/dist/shared/retrieval.js +9 -3
  28. package/mcp/dist/status.js +1 -1
  29. package/mcp/dist/store-registry.js +15 -1
  30. package/mcp/dist/tools/finding.js +114 -12
  31. package/mcp/dist/tools/memory.js +49 -4
  32. package/mcp/dist/tools/search.js +10 -1
  33. package/mcp/dist/tools/session.js +10 -4
  34. package/mcp/dist/tools/tasks.js +60 -1
  35. package/mcp/dist/tools/types.js +10 -0
  36. package/package.json +1 -1
  37. package/skills/sync/SKILL.md +1 -1
  38. package/starter/README.md +6 -6
  39. package/starter/machines.yaml +1 -1
  40. package/starter/my-first-project/tasks.md +1 -1
  41. package/starter/templates/README.md +1 -1
@@ -63,6 +63,29 @@ export async function handlePinCanonical(project, memory) {
63
63
  const result = upsertCanonical(getPhrenPath(), project, memory);
64
64
  console.log(result.ok ? result.data : result.error);
65
65
  }
66
+ export async function handleTruths(project) {
67
+ if (!project) {
68
+ console.error("Usage: phren truths <project>");
69
+ process.exit(1);
70
+ }
71
+ const phrenPath = getPhrenPath();
72
+ const truthsPath = path.join(phrenPath, project, "truths.md");
73
+ if (!fs.existsSync(truthsPath)) {
74
+ console.log(`No truths pinned for "${project}" yet.`);
75
+ console.log(`\nPin one: phren pin ${project} "your truth here"`);
76
+ return;
77
+ }
78
+ const content = fs.readFileSync(truthsPath, "utf8");
79
+ const truths = content.split("\n").filter((line) => line.startsWith("- "));
80
+ if (truths.length === 0) {
81
+ console.log(`No truths pinned for "${project}" yet.`);
82
+ return;
83
+ }
84
+ console.log(`${truths.length} truth(s) for "${project}":\n`);
85
+ for (const truth of truths) {
86
+ console.log(` ${truth}`);
87
+ }
88
+ }
66
89
  export async function handleDoctor(args) {
67
90
  const profile = resolveRuntimeProfile(getPhrenPath());
68
91
  const fix = args.includes("--fix");
@@ -291,7 +314,7 @@ export async function handleUpdate(args) {
291
314
  process.exitCode = 1;
292
315
  }
293
316
  else {
294
- console.log("Run 'npx phren init' to refresh hooks and config.");
317
+ console.log("Run 'phren init' to refresh hooks and config.");
295
318
  }
296
319
  }
297
320
  export async function handleReview(args) {
@@ -8,8 +8,9 @@ import { handleGovernMemories, handlePruneMemories, handleConsolidateMemories, h
8
8
  import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowPolicy, } from "./config.js";
9
9
  import { parseSearchArgs } from "./search.js";
10
10
  import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handlePromoteNamespace, handleStoreNamespace, handleTaskNamespace, } from "./namespaces.js";
11
+ import { handleTeamNamespace } from "./team.js";
11
12
  import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInjection, handleInspectIndex, } from "./ops.js";
12
- import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./actions.js";
13
+ import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handleTruths, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./actions.js";
13
14
  import { handleGraphNamespace } from "./graph.js";
14
15
  import { resolveRuntimeProfile } from "../runtime-profile.js";
15
16
  // ── CLI router ───────────────────────────────────────────────────────────────
@@ -107,8 +108,12 @@ export async function runCliCommand(command, args) {
107
108
  return handleConsolidationStatus(args);
108
109
  case "session-context":
109
110
  return handleSessionContext();
111
+ case "truths":
112
+ return handleTruths(args[0]);
110
113
  case "store":
111
114
  return handleStoreNamespace(args);
115
+ case "team":
116
+ return handleTeamNamespace(args);
112
117
  case "promote":
113
118
  return handlePromoteNamespace(args);
114
119
  default:
@@ -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)) {
@@ -1763,7 +1763,6 @@ export async function handlePromoteNamespace(args) {
1763
1763
  // Write to target store
1764
1764
  const targetProjectDir = path.join(targetStore.path, project);
1765
1765
  fs.mkdirSync(targetProjectDir, { recursive: true });
1766
- const targetFindingsPath = path.join(targetProjectDir, "FINDINGS.md");
1767
1766
  const { addFindingToFile } = await import("../shared/content.js");
1768
1767
  const result = addFindingToFile(targetStore.path, project, match.text);
1769
1768
  if (!result.ok) {
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Team store CLI commands: init, join, add-project.
3
+ * Creates and manages shared phren stores for team collaboration.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { execFileSync } from "child_process";
8
+ import { getPhrenPath } from "../shared.js";
9
+ import { isValidProjectName } from "../utils.js";
10
+ import { addStoreToRegistry, findStoreByName, generateStoreId, readTeamBootstrap, updateStoreProjects, } from "../store-registry.js";
11
+ const EXEC_TIMEOUT_MS = 30_000;
12
+ function getOptionValue(args, name) {
13
+ const exactIdx = args.indexOf(name);
14
+ if (exactIdx !== -1)
15
+ return args[exactIdx + 1];
16
+ const prefixed = args.find((arg) => arg.startsWith(`${name}=`));
17
+ return prefixed ? prefixed.slice(name.length + 1) : undefined;
18
+ }
19
+ function getPositionalArgs(args, optionNames) {
20
+ const positions = [];
21
+ for (let i = 0; i < args.length; i++) {
22
+ const arg = args[i];
23
+ if (optionNames.includes(arg)) {
24
+ i++;
25
+ continue;
26
+ }
27
+ if (optionNames.some((name) => arg.startsWith(`${name}=`)))
28
+ continue;
29
+ if (!arg.startsWith("--"))
30
+ positions.push(arg);
31
+ }
32
+ return positions;
33
+ }
34
+ function atomicWriteText(filePath, content) {
35
+ const dir = path.dirname(filePath);
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ const tmp = filePath + ".tmp." + process.pid;
38
+ fs.writeFileSync(tmp, content);
39
+ fs.renameSync(tmp, filePath);
40
+ }
41
+ // ── phren team init <name> [--remote <url>] [--description <desc>] ──────────
42
+ async function handleTeamInit(args) {
43
+ const phrenPath = getPhrenPath();
44
+ const positional = getPositionalArgs(args, ["--remote", "--description"]);
45
+ const name = positional[0];
46
+ const remote = getOptionValue(args, "--remote");
47
+ const description = getOptionValue(args, "--description");
48
+ if (!name) {
49
+ console.error("Usage: phren team init <name> [--remote <url>] [--description <text>]");
50
+ process.exit(1);
51
+ }
52
+ if (!isValidProjectName(name)) {
53
+ console.error(`Invalid store name: "${name}". Use lowercase letters, numbers, hyphens.`);
54
+ process.exit(1);
55
+ }
56
+ // Check if store already exists
57
+ const existing = findStoreByName(phrenPath, name);
58
+ if (existing) {
59
+ console.error(`Store "${name}" already exists at ${existing.path}`);
60
+ process.exit(1);
61
+ }
62
+ const storesDir = path.join(path.dirname(phrenPath), ".phren-stores");
63
+ const storePath = path.join(storesDir, name);
64
+ if (fs.existsSync(storePath)) {
65
+ console.error(`Directory already exists: ${storePath}`);
66
+ process.exit(1);
67
+ }
68
+ // Create the team store directory
69
+ fs.mkdirSync(storePath, { recursive: true });
70
+ // Write .phren-team.yaml
71
+ const bootstrap = {
72
+ name,
73
+ description: description || `${name} team knowledge`,
74
+ default_role: "team",
75
+ };
76
+ atomicWriteText(path.join(storePath, ".phren-team.yaml"), `name: ${bootstrap.name}\ndescription: ${bootstrap.description}\ndefault_role: team\n`);
77
+ // Write .gitignore
78
+ atomicWriteText(path.join(storePath, ".gitignore"), ".runtime/\n.sessions/\n*.lock\n");
79
+ // Create global directory with starter files
80
+ const globalDir = path.join(storePath, "global");
81
+ fs.mkdirSync(globalDir, { recursive: true });
82
+ atomicWriteText(path.join(globalDir, "CLAUDE.md"), `# ${name} Team Store\n\nShared knowledge for the ${name} team.\n`);
83
+ atomicWriteText(path.join(globalDir, "FINDINGS.md"), `# global findings\n`);
84
+ // Initialize git repo
85
+ execFileSync("git", ["init"], {
86
+ cwd: storePath,
87
+ stdio: "pipe",
88
+ timeout: EXEC_TIMEOUT_MS,
89
+ });
90
+ // Initial commit
91
+ execFileSync("git", ["add", "-A"], { cwd: storePath, stdio: "pipe", timeout: EXEC_TIMEOUT_MS });
92
+ execFileSync("git", ["commit", "-m", "phren: initialize team store"], {
93
+ cwd: storePath,
94
+ stdio: "pipe",
95
+ timeout: EXEC_TIMEOUT_MS,
96
+ });
97
+ // Add remote if provided
98
+ if (remote) {
99
+ execFileSync("git", ["remote", "add", "origin", remote], {
100
+ cwd: storePath,
101
+ stdio: "pipe",
102
+ timeout: EXEC_TIMEOUT_MS,
103
+ });
104
+ try {
105
+ execFileSync("git", ["push", "-u", "origin", "main"], {
106
+ cwd: storePath,
107
+ stdio: "pipe",
108
+ timeout: EXEC_TIMEOUT_MS,
109
+ });
110
+ console.log(` Pushed to ${remote}`);
111
+ }
112
+ catch {
113
+ // Try HEAD branch name
114
+ try {
115
+ execFileSync("git", ["push", "-u", "origin", "HEAD"], {
116
+ cwd: storePath,
117
+ stdio: "pipe",
118
+ timeout: EXEC_TIMEOUT_MS,
119
+ });
120
+ console.log(` Pushed to ${remote}`);
121
+ }
122
+ catch {
123
+ console.log(` Remote added but push failed. Push manually: cd ${storePath} && git push -u origin main`);
124
+ }
125
+ }
126
+ }
127
+ // Register in primary store's stores.yaml
128
+ const entry = {
129
+ id: generateStoreId(),
130
+ name,
131
+ path: storePath,
132
+ role: "team",
133
+ sync: "managed-git",
134
+ ...(remote ? { remote } : {}),
135
+ };
136
+ addStoreToRegistry(phrenPath, entry);
137
+ console.log(`\nCreated team store: ${name}`);
138
+ console.log(` Path: ${storePath}`);
139
+ console.log(` Role: team`);
140
+ console.log(` ID: ${entry.id}`);
141
+ if (!remote) {
142
+ console.log(`\nNext: add a remote and push`);
143
+ console.log(` cd ${storePath}`);
144
+ console.log(` git remote add origin <your-git-url>`);
145
+ console.log(` git push -u origin main`);
146
+ }
147
+ console.log(`\nAdd projects: phren team add-project ${name} <project-name>`);
148
+ }
149
+ // ── phren team join <url> [--name <name>] ───────────────────────────────────
150
+ async function handleTeamJoin(args) {
151
+ const phrenPath = getPhrenPath();
152
+ const positional = getPositionalArgs(args, ["--name"]);
153
+ const remote = positional[0];
154
+ const nameOverride = getOptionValue(args, "--name");
155
+ if (!remote) {
156
+ console.error("Usage: phren team join <git-url> [--name <name>]");
157
+ process.exit(1);
158
+ }
159
+ const storesDir = path.join(path.dirname(phrenPath), ".phren-stores");
160
+ // Infer name from URL if not provided
161
+ const inferredName = nameOverride || path.basename(remote, ".git").toLowerCase().replace(/[^a-z0-9_-]/g, "-");
162
+ if (!isValidProjectName(inferredName)) {
163
+ console.error(`Invalid store name: "${inferredName}". Use --name to specify a valid name.`);
164
+ process.exit(1);
165
+ }
166
+ const existing = findStoreByName(phrenPath, inferredName);
167
+ if (existing) {
168
+ console.error(`Store "${inferredName}" already exists at ${existing.path}`);
169
+ process.exit(1);
170
+ }
171
+ const storePath = path.join(storesDir, inferredName);
172
+ if (fs.existsSync(storePath)) {
173
+ console.error(`Directory already exists: ${storePath}`);
174
+ process.exit(1);
175
+ }
176
+ // Clone the remote
177
+ console.log(`Cloning ${remote}...`);
178
+ fs.mkdirSync(storesDir, { recursive: true });
179
+ execFileSync("git", ["clone", "--", remote, storePath], {
180
+ stdio: "inherit",
181
+ timeout: 60_000,
182
+ });
183
+ // Read .phren-team.yaml if present
184
+ const bootstrap = readTeamBootstrap(storePath);
185
+ const finalName = bootstrap?.name || inferredName;
186
+ const finalRole = bootstrap?.default_role === "primary" ? "team" : (bootstrap?.default_role || "team");
187
+ const entry = {
188
+ id: generateStoreId(),
189
+ name: finalName,
190
+ path: storePath,
191
+ role: finalRole,
192
+ sync: finalRole === "readonly" ? "pull-only" : "managed-git",
193
+ remote,
194
+ };
195
+ addStoreToRegistry(phrenPath, entry);
196
+ console.log(`\nJoined team store: ${finalName}`);
197
+ console.log(` Path: ${storePath}`);
198
+ console.log(` Role: ${finalRole}`);
199
+ console.log(` ID: ${entry.id}`);
200
+ if (bootstrap?.description) {
201
+ console.log(` Description: ${bootstrap.description}`);
202
+ }
203
+ }
204
+ // ── phren team add-project <store> <project> ────────────────────────────────
205
+ async function handleTeamAddProject(args) {
206
+ const phrenPath = getPhrenPath();
207
+ const positional = getPositionalArgs(args, []);
208
+ const storeName = positional[0];
209
+ const projectName = positional[1];
210
+ if (!storeName || !projectName) {
211
+ console.error("Usage: phren team add-project <store-name> <project-name>");
212
+ process.exit(1);
213
+ }
214
+ if (!isValidProjectName(projectName)) {
215
+ console.error(`Invalid project name: "${projectName}"`);
216
+ process.exit(1);
217
+ }
218
+ const store = findStoreByName(phrenPath, storeName);
219
+ if (!store) {
220
+ console.error(`Store "${storeName}" not found. Run 'phren store list' to see available stores.`);
221
+ process.exit(1);
222
+ }
223
+ if (store.role === "readonly") {
224
+ console.error(`Store "${storeName}" is read-only. Cannot add projects.`);
225
+ process.exit(1);
226
+ }
227
+ // Create project directory in the store
228
+ const projectDir = path.join(store.path, projectName);
229
+ const journalDir = path.join(projectDir, "journal");
230
+ fs.mkdirSync(journalDir, { recursive: true });
231
+ // Scaffold project files
232
+ if (!fs.existsSync(path.join(projectDir, "FINDINGS.md"))) {
233
+ atomicWriteText(path.join(projectDir, "FINDINGS.md"), `# ${projectName} findings\n`);
234
+ }
235
+ if (!fs.existsSync(path.join(projectDir, "tasks.md"))) {
236
+ atomicWriteText(path.join(projectDir, "tasks.md"), `# ${projectName} tasks\n\n## Active\n\n## Queue\n\n## Done\n`);
237
+ }
238
+ if (!fs.existsSync(path.join(projectDir, "summary.md"))) {
239
+ atomicWriteText(path.join(projectDir, "summary.md"), `# ${projectName}\n**What:** \n`);
240
+ }
241
+ // Update store's project claims in registry
242
+ const currentProjects = store.projects || [];
243
+ if (!currentProjects.includes(projectName)) {
244
+ updateStoreProjects(phrenPath, storeName, [...currentProjects, projectName]);
245
+ }
246
+ console.log(`Added project "${projectName}" to team store "${storeName}"`);
247
+ console.log(` Path: ${projectDir}`);
248
+ console.log(` Journal: ${journalDir}`);
249
+ console.log(`\nWrites to "${projectName}" will now route to "${storeName}" automatically.`);
250
+ }
251
+ // ── phren team list ─────────────────────────────────────────────────────────
252
+ async function handleTeamList() {
253
+ const phrenPath = getPhrenPath();
254
+ const { resolveAllStores } = await import("../store-registry.js");
255
+ const stores = resolveAllStores(phrenPath);
256
+ const teamStores = stores.filter((s) => s.role === "team");
257
+ if (teamStores.length === 0) {
258
+ console.log("No team stores registered.");
259
+ console.log("\nCreate one: phren team init <name> [--remote <url>]");
260
+ console.log("Join one: phren team join <git-url>");
261
+ return;
262
+ }
263
+ console.log(`${teamStores.length} team store(s):\n`);
264
+ for (const store of teamStores) {
265
+ const exists = fs.existsSync(store.path);
266
+ console.log(` ${store.name} (${store.id}) ${exists ? "" : "[missing]"}`);
267
+ console.log(` path: ${store.path}`);
268
+ if (store.remote)
269
+ console.log(` remote: ${store.remote}`);
270
+ if (store.projects?.length)
271
+ console.log(` projects: ${store.projects.join(", ")}`);
272
+ console.log();
273
+ }
274
+ }
275
+ // ── Entry point ─────────────────────────────────────────────────────────────
276
+ export async function handleTeamNamespace(args) {
277
+ const subcommand = args[0];
278
+ const subArgs = args.slice(1);
279
+ switch (subcommand) {
280
+ case "init":
281
+ return handleTeamInit(subArgs);
282
+ case "join":
283
+ return handleTeamJoin(subArgs);
284
+ case "add-project":
285
+ return handleTeamAddProject(subArgs);
286
+ case "list":
287
+ return handleTeamList();
288
+ default:
289
+ console.log(`phren team — manage shared team stores
290
+
291
+ phren team init <name> [--remote <url>] Create a new team store
292
+ phren team join <git-url> [--name <name>] Join an existing team store
293
+ phren team add-project <store> <project> Add a project to a team store
294
+ phren team list List team stores
295
+ `);
296
+ if (subcommand) {
297
+ console.error(`Unknown subcommand: ${subcommand}`);
298
+ process.exit(1);
299
+ }
300
+ }
301
+ }
@@ -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");
@@ -273,6 +273,24 @@ export async function handleHookSessionStart() {
273
273
  catch (err) {
274
274
  logger.debug("hookSessionStart trackSession", errorMessage(err));
275
275
  }
276
+ // Pull non-primary stores (team + readonly) so session starts with fresh data
277
+ try {
278
+ const { getNonPrimaryStores } = await import("./store-registry.js");
279
+ const otherStores = getNonPrimaryStores(phrenPath);
280
+ for (const store of otherStores) {
281
+ if (!fs.existsSync(store.path) || !fs.existsSync(path.join(store.path, ".git")))
282
+ continue;
283
+ try {
284
+ await runBestEffortGit(["pull", "--rebase", "--quiet"], store.path);
285
+ }
286
+ catch (err) {
287
+ debugLog(`session-start store-pull ${store.name}: ${errorMessage(err)}`);
288
+ }
289
+ }
290
+ }
291
+ catch {
292
+ // store-registry not available — skip silently
293
+ }
276
294
  updateRuntimeHealth(phrenPath, {
277
295
  lastSessionStartAt: startedAt,
278
296
  lastSync: {
@@ -485,18 +485,48 @@ export async function handleHookStop() {
485
485
  });
486
486
  appendAuditLog(phrenPath, "hook_stop", `status=saved-local detail=${JSON.stringify(syncDetail)}`);
487
487
  }); // end withFileLock(gitOpLockPath)
488
- // Pull non-primary stores (best-effort, non-blocking)
488
+ // Sync non-primary stores: commit+push team stores, pull-only readonly stores
489
489
  try {
490
490
  const { getNonPrimaryStores } = await import("./store-registry.js");
491
491
  const otherStores = getNonPrimaryStores(phrenPath);
492
492
  for (const store of otherStores) {
493
493
  if (!fs.existsSync(store.path) || !fs.existsSync(path.join(store.path, ".git")))
494
494
  continue;
495
- try {
496
- await runBestEffortGit(["pull", "--rebase", "--quiet"], store.path);
495
+ if (store.role === "team") {
496
+ // Team stores: stage team-safe files, commit, and push
497
+ try {
498
+ const storeStatus = await runBestEffortGit(["status", "--porcelain"], store.path);
499
+ if (storeStatus.ok && storeStatus.output) {
500
+ // Only stage journal/, tasks.md, truths.md, FINDINGS.md, summary.md — NOT .runtime/
501
+ await runBestEffortGit(["add", "--", "*/journal/*", "*/tasks.md", "*/truths.md", "*/FINDINGS.md", "*/summary.md", ".phren-team.yaml"], store.path);
502
+ const actor = process.env.PHREN_ACTOR || process.env.USER || "unknown";
503
+ const teamCommit = await runBestEffortGit(["commit", "-m", `phren: ${actor} team sync`], store.path);
504
+ if (teamCommit.ok) {
505
+ // Check for remote before pushing
506
+ const storeRemotes = await runBestEffortGit(["remote"], store.path);
507
+ if (storeRemotes.ok && storeRemotes.output?.trim()) {
508
+ const teamPush = await runBestEffortGit(["push"], store.path);
509
+ if (!teamPush.ok) {
510
+ // Try pull-rebase then push
511
+ await runBestEffortGit(["pull", "--rebase", "--quiet"], store.path);
512
+ await runBestEffortGit(["push"], store.path);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ }
518
+ catch (err) {
519
+ debugLog(`hook-stop team-store-sync ${store.name}: ${errorMessage(err)}`);
520
+ }
497
521
  }
498
- catch (err) {
499
- debugLog(`hook-stop store-pull ${store.name}: ${errorMessage(err)}`);
522
+ else {
523
+ // Readonly stores: pull only
524
+ try {
525
+ await runBestEffortGit(["pull", "--rebase", "--quiet"], store.path);
526
+ }
527
+ catch (err) {
528
+ debugLog(`hook-stop store-pull ${store.name}: ${errorMessage(err)}`);
529
+ }
500
530
  }
501
531
  }
502
532
  }
@@ -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);
@@ -87,6 +87,12 @@ const HELP_TOPICS = {
87
87
  phren store add <name> --remote <url> Add a team store
88
88
  phren store remove <name> Remove a store (local only)
89
89
  phren store sync Pull all stores
90
+ `,
91
+ team: `Team:
92
+ phren team init <name> [--remote <url>] Create a new team store
93
+ phren team join <git-url> [--name <name>] Join an existing team store
94
+ phren team add-project <store> <project> Add a project to a team store
95
+ phren team list List team stores
90
96
  `,
91
97
  env: `Environment variables:
92
98
  PHREN_PATH Override phren directory (default: ~/.phren)
@@ -174,7 +180,9 @@ const CLI_COMMANDS = [
174
180
  "review",
175
181
  "consolidation-status",
176
182
  "session-context",
183
+ "truths",
177
184
  "store",
185
+ "team",
178
186
  "promote",
179
187
  ];
180
188
  async function flushTopLevelOutput() {
@@ -265,7 +273,7 @@ export async function runTopLevelCommand(argv) {
265
273
  const phrenPath = defaultPhrenPath();
266
274
  const profile = (process.env.PHREN_PROFILE) || undefined;
267
275
  if (!fs.existsSync(phrenPath) || !fs.existsSync(path.join(phrenPath, ".config"))) {
268
- console.log("phren is not set up yet. Run: npx phren init");
276
+ console.log("phren is not set up yet. Run: phren init");
269
277
  return finish(1);
270
278
  }
271
279
  const ownership = ownershipArg
@@ -381,7 +389,7 @@ export async function runTopLevelCommand(argv) {
381
389
  const note = getVerifyOutcomeNote(phrenPath, result.checks);
382
390
  if (note)
383
391
  console.log(`\nNote: ${note}`);
384
- console.log(`\nRun \`npx phren init\` to fix setup issues.`);
392
+ console.log(`\nRun \`phren init\` to fix setup issues.`);
385
393
  }
386
394
  return finish(result.ok ? 0 : 1);
387
395
  }
@@ -408,7 +416,7 @@ export async function runTopLevelCommand(argv) {
408
416
  }
409
417
  }
410
418
  if (argvCommand === "link") {
411
- console.error("`phren link` has been removed. Use `npx phren init` instead.");
419
+ console.error("`phren link` has been removed. Use `phren init` instead.");
412
420
  return finish(1);
413
421
  }
414
422
  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
  }
@@ -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",