@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.
- package/mcp/dist/cli/actions.js +24 -1
- package/mcp/dist/cli/cli.js +6 -1
- package/mcp/dist/cli/hooks-session.js +8 -8
- package/mcp/dist/cli/namespaces.js +3 -4
- package/mcp/dist/cli/team.js +301 -0
- package/mcp/dist/cli-hooks-session-handlers.js +26 -8
- package/mcp/dist/cli-hooks-stop.js +35 -5
- package/mcp/dist/content/dedup.js +5 -2
- package/mcp/dist/entrypoint.js +11 -3
- package/mcp/dist/finding/context.js +3 -2
- package/mcp/dist/init/config.js +1 -1
- package/mcp/dist/init/init-configure.js +1 -1
- package/mcp/dist/init/init-hooks-mode.js +1 -1
- package/mcp/dist/init/init-mcp-mode.js +2 -2
- package/mcp/dist/init/init-walkthrough.js +9 -9
- package/mcp/dist/init/init.js +8 -8
- package/mcp/dist/init/setup.js +46 -1
- package/mcp/dist/init-fresh.js +4 -4
- package/mcp/dist/init-hooks.js +1 -1
- package/mcp/dist/init-modes.js +3 -3
- package/mcp/dist/init-update.js +3 -3
- package/mcp/dist/init-walkthrough.js +9 -9
- package/mcp/dist/link/doctor.js +1 -1
- package/mcp/dist/link/link.js +1 -1
- package/mcp/dist/phren-paths.js +2 -2
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/shared/retrieval.js +9 -3
- package/mcp/dist/status.js +1 -1
- package/mcp/dist/store-registry.js +15 -1
- package/mcp/dist/tools/finding.js +114 -12
- package/mcp/dist/tools/memory.js +49 -4
- package/mcp/dist/tools/search.js +10 -1
- package/mcp/dist/tools/session.js +10 -4
- package/mcp/dist/tools/tasks.js +60 -1
- package/mcp/dist/tools/types.js +10 -0
- package/package.json +1 -1
- package/skills/sync/SKILL.md +1 -1
- package/starter/README.md +6 -6
- package/starter/machines.yaml +1 -1
- package/starter/my-first-project/tasks.md +1 -1
- package/starter/templates/README.md +1 -1
package/mcp/dist/cli/actions.js
CHANGED
|
@@ -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 '
|
|
317
|
+
console.log("Run 'phren init' to refresh hooks and config.");
|
|
295
318
|
}
|
|
296
319
|
}
|
|
297
320
|
export async function handleReview(args) {
|
package/mcp/dist/cli/cli.js
CHANGED
|
@@ -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 `
|
|
90
|
-
`Suggested command: \`
|
|
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 `
|
|
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 \`
|
|
95
|
-
"After onboarding, run `
|
|
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 `
|
|
131
|
-
"Run `
|
|
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 `
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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 `
|
|
64
|
-
`Suggested command: \`
|
|
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 `
|
|
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 \`
|
|
69
|
-
"After onboarding, run `
|
|
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 `
|
|
87
|
-
"Run `
|
|
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 `
|
|
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
|
-
//
|
|
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
|
-
|
|
496
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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.
|
|
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.
|
|
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);
|
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -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:
|
|
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 \`
|
|
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 `
|
|
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
|
-
|
|
171
|
-
|
|
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
|
}
|
package/mcp/dist/init/config.js
CHANGED
|
@@ -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",
|
|
210
|
+
const toolHookEnabled = hooksEnabled && isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", true);
|
|
211
211
|
if (hooksEnabled) {
|
|
212
212
|
upsertPhrenHook("UserPromptSubmit", {
|
|
213
213
|
type: "command",
|