@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
|
@@ -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.
|
|
21
|
-
const JACCARD_MAYBE_HIGH = 0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|
package/mcp/dist/tools/memory.js
CHANGED
|
@@ -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 {
|
|
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);
|
package/mcp/dist/tools/search.js
CHANGED
|
@@ -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
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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,
|
package/mcp/dist/tools/tasks.js
CHANGED
|
@@ -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)) {
|
package/mcp/dist/tools/types.js
CHANGED
|
@@ -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
package/skills/sync/SKILL.md
CHANGED
|
@@ -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"
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
74
|
+
Run `phren doctor --fix` to check and repair your setup.
|
package/starter/machines.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# starter/templates/
|
|
2
2
|
|
|
3
|
-
Project templates bundled in the npm package, used by `
|
|
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
|
|