@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
@@ -17,8 +17,8 @@ import { getActiveTaskForSession } from "../task/lifecycle.js";
17
17
  import { FINDING_PROVENANCE_SOURCES } from "../content/citation.js";
18
18
  import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "../finding/lifecycle.js";
19
19
  import { permissionDeniedError } from "../governance/rbac.js";
20
- const JACCARD_MAYBE_LOW = 0.30;
21
- const JACCARD_MAYBE_HIGH = 0.55; // above this isDuplicateFinding already catches it
20
+ const JACCARD_MAYBE_LOW = 0.25;
21
+ const JACCARD_MAYBE_HIGH = 0.40; // above this isDuplicateFinding already catches it
22
22
  function findJaccardCandidates(phrenPath, project, finding) {
23
23
  try {
24
24
  const findingsPath = path.join(phrenPath, project, "FINDINGS.md");
@@ -263,22 +263,52 @@ async function handleAddFinding(ctx, params) {
263
263
  }
264
264
  });
265
265
  }
266
- async function handleSupersedeFinding(ctx, { project, finding_text, superseded_by }) {
267
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
266
+ async function handleSupersedeFinding(ctx, { project: projectInput, finding_text, superseded_by }) {
267
+ let phrenPath;
268
+ let project;
269
+ try {
270
+ const resolved = resolveStoreForProject(ctx, projectInput);
271
+ phrenPath = resolved.phrenPath;
272
+ project = resolved.project;
273
+ }
274
+ catch (err) {
275
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
276
+ }
277
+ const { withWriteQueue, updateFileInIndex } = ctx;
268
278
  return withLifecycleMutation(phrenPath, project, withWriteQueue, updateFileInIndex, () => supersedeFinding(phrenPath, project, finding_text, superseded_by), (data) => ({
269
279
  message: `Marked finding as superseded in ${project}.`,
270
280
  data: { project, finding: data.finding, status: data.status, superseded_by: data.superseded_by },
271
281
  }));
272
282
  }
273
- async function handleRetractFinding(ctx, { project, finding_text, reason }) {
274
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
283
+ async function handleRetractFinding(ctx, { project: projectInput, finding_text, reason }) {
284
+ let phrenPath;
285
+ let project;
286
+ try {
287
+ const resolved = resolveStoreForProject(ctx, projectInput);
288
+ phrenPath = resolved.phrenPath;
289
+ project = resolved.project;
290
+ }
291
+ catch (err) {
292
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
293
+ }
294
+ const { withWriteQueue, updateFileInIndex } = ctx;
275
295
  return withLifecycleMutation(phrenPath, project, withWriteQueue, updateFileInIndex, () => retractFindingLifecycle(phrenPath, project, finding_text, reason), (data) => ({
276
296
  message: `Retracted finding in ${project}.`,
277
297
  data: { project, finding: data.finding, status: data.status, reason: data.reason },
278
298
  }));
279
299
  }
280
- async function handleResolveContradiction(ctx, { project, finding_text, finding_text_other, finding_a, finding_b, resolution }) {
281
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
300
+ async function handleResolveContradiction(ctx, { project: projectInput, finding_text, finding_text_other, finding_a, finding_b, resolution }) {
301
+ let phrenPath;
302
+ let project;
303
+ try {
304
+ const resolved = resolveStoreForProject(ctx, projectInput);
305
+ phrenPath = resolved.phrenPath;
306
+ project = resolved.project;
307
+ }
308
+ catch (err) {
309
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
310
+ }
311
+ const { withWriteQueue, updateFileInIndex } = ctx;
282
312
  const findingText = (finding_text ?? finding_a)?.trim();
283
313
  const findingTextOther = (finding_text_other ?? finding_b)?.trim();
284
314
  if (!findingText || !findingTextOther) {
@@ -342,8 +372,18 @@ async function handleGetContradictions(ctx, { project, finding_text }) {
342
372
  },
343
373
  });
344
374
  }
345
- async function handleEditFinding(ctx, { project, old_text, new_text }) {
346
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
375
+ async function handleEditFinding(ctx, { project: projectInput, old_text, new_text }) {
376
+ let phrenPath;
377
+ let project;
378
+ try {
379
+ const resolved = resolveStoreForProject(ctx, projectInput);
380
+ phrenPath = resolved.phrenPath;
381
+ project = resolved.project;
382
+ }
383
+ catch (err) {
384
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
385
+ }
386
+ const { withWriteQueue, updateFileInIndex } = ctx;
347
387
  if (!isValidProjectName(project))
348
388
  return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
349
389
  const editDenied = permissionDeniedError(phrenPath, "edit_finding", project);
@@ -363,8 +403,18 @@ async function handleEditFinding(ctx, { project, old_text, new_text }) {
363
403
  });
364
404
  });
365
405
  }
366
- async function handleRemoveFinding(ctx, { project, finding }) {
367
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
406
+ async function handleRemoveFinding(ctx, { project: projectInput, finding }) {
407
+ let phrenPath;
408
+ let project;
409
+ try {
410
+ const resolved = resolveStoreForProject(ctx, projectInput);
411
+ phrenPath = resolved.phrenPath;
412
+ project = resolved.project;
413
+ }
414
+ catch (err) {
415
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
416
+ }
417
+ const { withWriteQueue, updateFileInIndex } = ctx;
368
418
  if (!isValidProjectName(project))
369
419
  return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
370
420
  const removeDenied = permissionDeniedError(phrenPath, "remove_finding", project);
@@ -494,6 +544,58 @@ async function handlePushChanges(ctx, { message }) {
494
544
  catch (err) {
495
545
  return mcpResponse({ ok: false, error: `Save failed: ${errorMessage(err)}`, errorCode: "INTERNAL_ERROR" });
496
546
  }
547
+ // Sync team stores: commit and push journal/tasks/truths changes
548
+ try {
549
+ const { getNonPrimaryStores } = await import("../store-registry.js");
550
+ const teamStores = getNonPrimaryStores(phrenPath).filter((s) => s.role === "team");
551
+ const teamResults = [];
552
+ for (const store of teamStores) {
553
+ if (!fs.existsSync(store.path) || !fs.existsSync(path.join(store.path, ".git")))
554
+ continue;
555
+ const runStoreGit = (args, opts = {}) => execFileSync("git", args, {
556
+ cwd: store.path,
557
+ encoding: "utf8",
558
+ timeout: opts.timeout ?? EXEC_TIMEOUT_MS,
559
+ env: opts.env,
560
+ stdio: ["ignore", "pipe", "pipe"],
561
+ }).trim();
562
+ try {
563
+ const storeStatus = runStoreGit(["status", "--porcelain"]);
564
+ if (!storeStatus) {
565
+ teamResults.push({ store: store.name, pushed: false });
566
+ continue;
567
+ }
568
+ // Only stage team-safe files: journal/, tasks.md, truths.md, FINDINGS.md, summary.md
569
+ runStoreGit(["add", "--", "*/journal/*", "*/tasks.md", "*/truths.md", "*/FINDINGS.md", "*/summary.md"]);
570
+ const actor = process.env.PHREN_ACTOR || process.env.USER || "unknown";
571
+ runStoreGit(["commit", "-m", `phren: ${actor} team sync`]);
572
+ try {
573
+ runStoreGit(["push"], { timeout: 15000 });
574
+ teamResults.push({ store: store.name, pushed: true });
575
+ }
576
+ catch {
577
+ try {
578
+ runStoreGit(["pull", "--rebase", "--quiet"], { timeout: 15000 });
579
+ runStoreGit(["push"], { timeout: 15000 });
580
+ teamResults.push({ store: store.name, pushed: true });
581
+ }
582
+ catch (retryErr) {
583
+ teamResults.push({ store: store.name, pushed: false, error: errorMessage(retryErr) });
584
+ }
585
+ }
586
+ }
587
+ catch (storeErr) {
588
+ teamResults.push({ store: store.name, pushed: false, error: errorMessage(storeErr) });
589
+ }
590
+ }
591
+ // Team store results are best-effort — don't fail the primary push for them
592
+ if (teamResults.length > 0) {
593
+ debugLog(`push_changes team stores: ${JSON.stringify(teamResults)}`);
594
+ }
595
+ }
596
+ catch {
597
+ // store-registry not available — skip silently
598
+ }
497
599
  });
498
600
  }
499
601
  // ── Registration ─────────────────────────────────────────────────────────────
@@ -1,4 +1,4 @@
1
- import { mcpResponse } from "./types.js";
1
+ import { mcpResponse, resolveStoreForProject } from "./types.js";
2
2
  import { z } from "zod";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
@@ -7,7 +7,7 @@ import { recordFeedback, flushEntryScores, } from "../shared/governance.js";
7
7
  import { upsertCanonical } from "../shared/content.js";
8
8
  import { isValidProjectName } from "../utils.js";
9
9
  export function register(server, ctx) {
10
- const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
10
+ const { withWriteQueue, updateFileInIndex } = ctx;
11
11
  server.registerTool("pin_memory", {
12
12
  title: "◆ phren · pin memory",
13
13
  description: "Write a truth — a high-confidence, always-inject entry in truths.md that never decays.",
@@ -15,19 +15,63 @@ export function register(server, ctx) {
15
15
  project: z.string().describe("Project name."),
16
16
  memory: z.string().describe("Truth text."),
17
17
  }),
18
- }, async ({ project, memory }) => {
18
+ }, async ({ project: projectInput, memory }) => {
19
+ let phrenPath;
20
+ let project;
21
+ try {
22
+ const resolved = resolveStoreForProject(ctx, projectInput);
23
+ phrenPath = resolved.phrenPath;
24
+ project = resolved.project;
25
+ }
26
+ catch (err) {
27
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
28
+ }
19
29
  if (!isValidProjectName(project))
20
30
  return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
21
31
  return withWriteQueue(async () => {
22
32
  const result = upsertCanonical(phrenPath, project, memory);
23
33
  if (!result.ok)
24
34
  return mcpResponse({ ok: false, error: result.error });
25
- // Update FTS index so newly added truth is immediately searchable
26
35
  const canonicalPath = path.join(phrenPath, project, "truths.md");
27
36
  updateFileInIndex(canonicalPath);
28
37
  return mcpResponse({ ok: true, message: result.data, data: { project, memory } });
29
38
  });
30
39
  });
40
+ server.registerTool("get_truths", {
41
+ title: "◆ phren · truths",
42
+ description: "Read all pinned truths for a project. Truths are high-confidence entries in truths.md that never decay.",
43
+ inputSchema: z.object({
44
+ project: z.string().describe("Project name."),
45
+ }),
46
+ }, async ({ project: projectInput }) => {
47
+ let phrenPath;
48
+ let project;
49
+ try {
50
+ const resolved = resolveStoreForProject(ctx, projectInput);
51
+ phrenPath = resolved.phrenPath;
52
+ project = resolved.project;
53
+ }
54
+ catch (err) {
55
+ return mcpResponse({ ok: false, error: err instanceof Error ? err.message : String(err) });
56
+ }
57
+ if (!isValidProjectName(project))
58
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
59
+ const truthsPath = path.join(phrenPath, project, "truths.md");
60
+ if (!fs.existsSync(truthsPath)) {
61
+ return mcpResponse({ ok: true, message: `No truths pinned for "${project}" yet.`, data: { project, truths: [], count: 0 } });
62
+ }
63
+ const content = fs.readFileSync(truthsPath, "utf8");
64
+ const truths = content.split("\n")
65
+ .filter((line) => line.startsWith("- "))
66
+ .map((line) => line.slice(2).trim());
67
+ return mcpResponse({
68
+ ok: true,
69
+ message: truths.length > 0
70
+ ? `${truths.length} truth(s) pinned for "${project}".`
71
+ : `No truths pinned for "${project}" yet.`,
72
+ data: { project, truths, count: truths.length },
73
+ });
74
+ });
31
75
  server.registerTool("memory_feedback", {
32
76
  title: "◆ phren · feedback",
33
77
  description: "Record feedback on whether an injected memory was helpful or noisy/regressive.",
@@ -36,6 +80,7 @@ export function register(server, ctx) {
36
80
  feedback: z.enum(["helpful", "reprompt", "regression"]).describe("Feedback type."),
37
81
  }),
38
82
  }, async ({ key, feedback }) => {
83
+ const phrenPath = ctx.phrenPath;
39
84
  return withWriteQueue(async () => {
40
85
  recordFeedback(phrenPath, key, feedback);
41
86
  flushEntryScores(phrenPath);
@@ -470,7 +470,7 @@ async function handleGetProjectSummary(ctx, { name }) {
470
470
  if (store && fs.existsSync(path.join(store.path, lookupName))) {
471
471
  const projDir = path.join(store.path, lookupName);
472
472
  const fsDocs = [];
473
- for (const [file, type] of [["summary.md", "summary"], ["CLAUDE.md", "claude"], ["FINDINGS.md", "findings"], ["tasks.md", "task"]]) {
473
+ for (const [file, type] of [["summary.md", "summary"], ["CLAUDE.md", "claude"], ["FINDINGS.md", "findings"], ["tasks.md", "task"], ["truths.md", "canonical"]]) {
474
474
  const filePath = path.join(projDir, file);
475
475
  if (fs.existsSync(filePath)) {
476
476
  fsDocs.push({ filename: file, type, content: fs.readFileSync(filePath, "utf8").slice(0, 8000), path: filePath });
@@ -487,6 +487,7 @@ async function handleGetProjectSummary(ctx, { name }) {
487
487
  }
488
488
  const summaryDoc = docs.find(doc => doc.type === "summary");
489
489
  const claudeDoc = docs.find(doc => doc.type === "claude");
490
+ const canonicalDoc = docs.find(doc => doc.type === "canonical");
490
491
  const indexedFiles = docs.map(doc => ({ filename: doc.filename, type: doc.type, path: doc.path }));
491
492
  const parts = [`# ${name}`];
492
493
  if (summaryDoc) {
@@ -498,6 +499,13 @@ async function handleGetProjectSummary(ctx, { name }) {
498
499
  if (claudeDoc) {
499
500
  parts.push(`\n## CLAUDE.md path\n\`${claudeDoc.path}\``);
500
501
  }
502
+ // Show truths if they exist
503
+ if (canonicalDoc) {
504
+ const truthLines = canonicalDoc.content.split("\n").filter((l) => l.startsWith("- "));
505
+ if (truthLines.length > 0) {
506
+ parts.push(`\n## Truths (${truthLines.length})\n${truthLines.join("\n")}`);
507
+ }
508
+ }
501
509
  const fileList = indexedFiles.map((f) => `- ${f.filename} (${f.type})`).join("\n");
502
510
  parts.push(`\n## Indexed files\n${fileList}`);
503
511
  return mcpResponse({
@@ -507,6 +515,7 @@ async function handleGetProjectSummary(ctx, { name }) {
507
515
  name,
508
516
  summary: summaryDoc?.content ?? null,
509
517
  claudeMdPath: claudeDoc?.path ?? null,
518
+ truthsPath: canonicalDoc?.path ?? null,
510
519
  files: indexedFiles,
511
520
  },
512
521
  });
@@ -18,6 +18,7 @@ import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints
18
18
  import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
19
19
  import { atomicWriteJson, debugError, scanSessionFiles } from "../session/utils.js";
20
20
  import { getRuntimeHealth } from "../governance/policy.js";
21
+ import { getProjectSourcePath, readProjectConfig } from "../project-config.js";
21
22
  const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
22
23
  function collectGitStatusSnapshot(cwd) {
23
24
  try {
@@ -221,9 +222,11 @@ function cleanupStaleSessions(phrenPath) {
221
222
  // (no endedAt) should never be removed regardless of age.
222
223
  if (state && !state.endedAt)
223
224
  continue;
224
- // prefer startedAt from the JSON content over mtime (reliable on noatime mounts)
225
- const ageMs = state?.startedAt
226
- ? Date.now() - new Date(state.startedAt).getTime()
225
+ // For ended sessions, age out by end time rather than start time so
226
+ // long-running sessions do not disappear immediately after they finish.
227
+ const expirationAnchor = state?.endedAt || state?.startedAt;
228
+ const ageMs = expirationAnchor
229
+ ? Date.now() - new Date(expirationAnchor).getTime()
227
230
  : Date.now() - fs.statSync(fullPath).mtimeMs;
228
231
  if (ageMs > STALE_SESSION_MS) {
229
232
  fs.unlinkSync(fullPath);
@@ -607,7 +610,10 @@ export function register(server, ctx) {
607
610
  })();
608
611
  if (activeTask) {
609
612
  const taskId = activeTask.stableId || activeTask.id;
610
- const { gitStatus, editedFiles } = collectGitStatusSnapshot(process.cwd());
613
+ const projectConfig = readProjectConfig(phrenPath, endedState.project);
614
+ const snapshotRoot = getProjectSourcePath(phrenPath, endedState.project, projectConfig) ||
615
+ path.join(phrenPath, endedState.project);
616
+ const { gitStatus, editedFiles } = collectGitStatusSnapshot(snapshotRoot);
611
617
  const resumptionHint = extractResumptionHint(effectiveSummary, activeTask.line, activeTask.context || "No prior attempt captured");
612
618
  writeTaskCheckpoint(phrenPath, {
613
619
  project: endedState.project,
@@ -5,7 +5,7 @@ import * as path from "path";
5
5
  import { isValidProjectName } from "../utils.js";
6
6
  import { addTask as addTaskStore, addTasks as addTasksBatch, taskMarkdown, completeTask as completeTaskStore, completeTasks as completeTasksBatch, removeTask as removeTaskStore, removeTasks as removeTasksBatch, linkTaskIssue, pinTask, workNextTask, tidyDoneTasks, readTasks, readTasksAcrossProjects, resolveTaskItem, TASKS_FILENAME, updateTask as updateTaskStore, promoteTask, } from "../data/access.js";
7
7
  import { applyGravity } from "../data/tasks.js";
8
- import { parseGithubIssueUrl, } from "../task/github.js";
8
+ import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo, } from "../task/github.js";
9
9
  import { clearTaskCheckpoint } from "../session/checkpoints.js";
10
10
  import { incrementSessionTasksCompleted } from "./session.js";
11
11
  import { normalizeMemoryScope } from "../shared.js";
@@ -366,6 +366,7 @@ export function register(server, ctx) {
366
366
  github_issue: z.union([z.number().int().positive(), z.string()]).optional().describe("GitHub issue number (for example 14 or '#14')."),
367
367
  github_url: z.string().optional().describe("GitHub issue URL to associate with the task item."),
368
368
  unlink_github: z.boolean().optional().describe("If true, remove any linked GitHub issue metadata from the item."),
369
+ create_issue: z.boolean().optional().describe("If true, create a GitHub issue for this task and link it."),
369
370
  pin: z.boolean().optional().describe("If true, pin the task so it floats to the top of its section."),
370
371
  promote: z.boolean().optional().describe("If true, clear the speculative flag on this task (confirm the user wants it)."),
371
372
  move_to_active: z.boolean().optional().describe("Used with promote: also move the task to the Active section."),
@@ -382,6 +383,25 @@ export function register(server, ctx) {
382
383
  if (!updates.work_next && !item) {
383
384
  return mcpResponse({ ok: false, error: "item is required unless updates.work_next is true." });
384
385
  }
386
+ if (updates.create_issue) {
387
+ const extraUpdates = [
388
+ updates.text,
389
+ updates.priority,
390
+ updates.context,
391
+ updates.section,
392
+ updates.github_issue,
393
+ updates.github_url,
394
+ updates.unlink_github,
395
+ updates.pin,
396
+ updates.promote,
397
+ updates.move_to_active,
398
+ updates.work_next,
399
+ updates.replace_context,
400
+ ].some((value) => value !== undefined);
401
+ if (extraUpdates) {
402
+ return mcpResponse({ ok: false, error: "create_issue must be used by itself." });
403
+ }
404
+ }
385
405
  // Cross-validate github_issue and github_url
386
406
  if (updates.github_url) {
387
407
  const parsed = parseGithubIssueUrl(updates.github_url);
@@ -423,6 +443,45 @@ export function register(server, ctx) {
423
443
  data: { project, item: result.data },
424
444
  });
425
445
  }
446
+ if (updates.create_issue) {
447
+ const resolved = resolveTaskItem(phrenPath, project, item);
448
+ if (!resolved.ok)
449
+ return mcpResponse({ ok: false, error: resolved.error });
450
+ const repo = resolveProjectGithubRepo(phrenPath, project);
451
+ if (!repo) {
452
+ return mcpResponse({
453
+ ok: false,
454
+ error: "Could not infer a GitHub repo. Add a GitHub URL to CLAUDE.md or summary.md, or link an existing issue instead.",
455
+ });
456
+ }
457
+ const created = createGithubIssueForTask({
458
+ repo,
459
+ title: resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
460
+ body: buildTaskIssueBody(project, resolved.data),
461
+ });
462
+ if (!created.ok)
463
+ return mcpResponse({ ok: false, error: created.error, errorCode: created.code });
464
+ const linked = linkTaskIssue(phrenPath, project, item, {
465
+ github_issue: created.data.issueNumber,
466
+ github_url: created.data.url,
467
+ });
468
+ if (!linked.ok)
469
+ return mcpResponse({ ok: false, error: linked.error, errorCode: linked.code });
470
+ refreshTaskIndex(updateFileInIndex, phrenPath, project);
471
+ return mcpResponse({
472
+ ok: true,
473
+ message: `Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`,
474
+ data: {
475
+ project,
476
+ item,
477
+ issue_number: created.data.issueNumber ?? null,
478
+ issue_url: created.data.url,
479
+ githubIssue: linked.data.githubIssue ?? null,
480
+ githubUrl: linked.data.githubUrl || null,
481
+ stableId: linked.data.stableId || null,
482
+ },
483
+ });
484
+ }
426
485
  // Handle github issue linking via update_task when github_issue or github_url is set (and no other field updates)
427
486
  if ((updates.github_issue !== undefined || updates.github_url || updates.unlink_github) && !updates.text && !updates.priority && !updates.context && !updates.section) {
428
487
  if (updates.unlink_github && (updates.github_issue !== undefined || updates.github_url)) {
@@ -8,6 +8,16 @@ import { resolveAllStores } from "../store-registry.js";
8
8
  export function resolveStoreForProject(ctx, projectInput) {
9
9
  const { storeName, projectName } = parseStoreQualified(projectInput);
10
10
  if (!storeName) {
11
+ // Check if any non-readonly store claims this project via projects[] array.
12
+ // This enables automatic write routing: once a project is claimed by a team
13
+ // store (via `phren team add-project`), writes go there without needing the
14
+ // store-qualified prefix.
15
+ const stores = resolveAllStores(ctx.phrenPath);
16
+ for (const store of stores) {
17
+ if (store.role !== "readonly" && store.role !== "primary" && store.projects?.includes(projectName)) {
18
+ return { phrenPath: store.path, project: projectName, storeRole: store.role };
19
+ }
20
+ }
11
21
  return { phrenPath: ctx.phrenPath, project: projectName, storeRole: "primary" };
12
22
  }
13
23
  const stores = resolveAllStores(ctx.phrenPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -102,7 +102,7 @@ If this fails (not a git repo, no remote), tell the user. Don't silently skip.
102
102
  Run init against the pulled phren repo so hooks, MCP registration, and machine/profile wiring are refreshed:
103
103
 
104
104
  ```bash
105
- PHREN_PATH="$PHREN_DIR" npx phren init -y
105
+ PHREN_PATH="$PHREN_DIR" phren init -y
106
106
  ```
107
107
 
108
108
  If the user is in an untracked repo afterward, tell them to open a session there and let the agent ask, or run `phren add` from that directory.
package/starter/README.md CHANGED
@@ -32,7 +32,7 @@ New to phren? Here's what each file does and when it matters.
32
32
 
33
33
  **FINDINGS.md** fills itself. As Claude discovers insights, patterns, and decisions during your sessions, it tells phren and entries land here grouped by date. Old entries fade from retrieval over time. Wrong entries can be removed with `remove_finding()`.
34
34
 
35
- **tasks.md** is your task board file. It keeps Active (working now), Queue (up next), and Done (finished) in one place so the work history stays with the project. You can also manage it from `npx phren shell`.
35
+ **tasks.md** is your task board file. It keeps Active (working now), Queue (up next), and Done (finished) in one place so the work history stays with the project. You can also manage it from `phren shell`.
36
36
 
37
37
  **global/CLAUDE.md** applies everywhere. Your style preferences, tool choices, things Claude should always know regardless of which project you're in.
38
38
 
@@ -40,14 +40,14 @@ New to phren? Here's what each file does and when it matters.
40
40
 
41
41
  ## Getting started
42
42
 
43
- If you got here via `npx phren init`, you're already set up. Restart Claude Code and you're good.
43
+ If you got here via `npx @phren/cli init`, you're already set up. Restart Claude Code and you're good.
44
44
 
45
45
  If you cloned manually:
46
46
 
47
- 1. Add the MCP server: `claude mcp add phren -- npx phren ~/.phren`
47
+ 1. Add the MCP server: `claude mcp add phren -- npx @phren/cli ~/.phren`
48
48
  2. Install skills: `/plugin marketplace add alaarab/phren` then `/plugin install phren@phren`
49
49
  3. Restart Claude Code
50
- 4. Add a project: run `/phren-init my-project` or scaffold one with a template such as `npx phren init --template python-project`
50
+ 4. Add a project: run `/phren-init my-project` or scaffold one with a template such as `phren init --template python-project`
51
51
  5. Push to a private GitHub repo to sync across machines
52
52
 
53
53
  ## Day-to-day workflow
@@ -56,7 +56,7 @@ If you cloned manually:
56
56
  2. **Work normally**: Claude reads your project docs and builds on what phren remembers
57
57
  3. **Fragments accumulate**: tell phren what you learned, or he picks up insights automatically
58
58
  4. **Session ends**: phren commits and pushes what he collected
59
- 5. **Review occasionally**: run `npx phren shell` to triage what phren queued, manage tasks, and check health
59
+ 5. **Review occasionally**: run `phren shell` to triage what phren queued, manage tasks, and check health
60
60
 
61
61
  ## Syncing across machines
62
62
 
@@ -71,4 +71,4 @@ Each profile in `profiles/` lists which projects that machine should see. After
71
71
 
72
72
  ## Troubleshooting
73
73
 
74
- Run `npx phren doctor --fix` to check and repair your setup.
74
+ Run `phren doctor --fix` to check and repair your setup.
@@ -5,4 +5,4 @@
5
5
  # work-laptop: work
6
6
  # home-desktop: personal
7
7
  #
8
- # After adding a line, run: npx phren init
8
+ # After adding a line, run: phren init
@@ -22,4 +22,4 @@
22
22
 
23
23
  ## Done
24
24
 
25
- - [x] Run `npx phren init` to set up phren
25
+ - [x] Run `phren init` to set up phren
@@ -1,6 +1,6 @@
1
1
  # starter/templates/
2
2
 
3
- Project templates bundled in the npm package, used by `npx phren init --template <name>`.
3
+ Project templates bundled in the npm package, used by `phren init --template <name>`.
4
4
 
5
5
  Each subdirectory (frontend, library, monorepo, python-project) contains pre-filled project files with sensible defaults for that project type. When a user runs init with `--template`, these files are copied into their `~/.phren/<project>/` directory. For other project types, adaptive init infers topics and structure from repo content.
6
6