@phren/cli 0.0.8 → 0.0.10
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/icon.svg +416 -0
- package/mcp/dist/capabilities/mcp.js +5 -5
- package/mcp/dist/capabilities/vscode.js +3 -3
- package/mcp/dist/cli-actions.js +113 -3
- package/mcp/dist/cli-govern.js +166 -1
- package/mcp/dist/cli-hooks.js +2 -2
- package/mcp/dist/cli-namespaces.js +497 -4
- package/mcp/dist/cli.js +7 -1
- package/mcp/dist/content-citation.js +12 -22
- package/mcp/dist/data-access.js +1 -1
- package/mcp/dist/data-tasks.js +39 -1
- package/mcp/dist/entrypoint.js +6 -0
- package/mcp/dist/mcp-finding.js +26 -1
- package/mcp/dist/mcp-tasks.js +23 -2
- package/mcp/dist/memory-ui-assets.js +37 -1
- package/mcp/dist/memory-ui-graph.js +19 -5
- package/mcp/dist/memory-ui-page.js +7 -30
- package/mcp/dist/memory-ui-scripts.js +1 -103
- package/mcp/dist/memory-ui-server.js +1 -82
- package/mcp/dist/shared-retrieval.js +7 -47
- package/mcp/dist/shell-input.js +4 -22
- package/mcp/dist/shell-render.js +2 -2
- package/mcp/dist/shell-view.js +1 -2
- package/mcp/dist/shell.js +0 -5
- package/mcp/dist/tool-registry.js +1 -0
- package/package.json +2 -1
- package/skills/docs.md +170 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execFileSync } from "child_process";
|
|
4
|
-
import { expandHomePath, getPhrenPath, getProjectDirs, homePath, hookConfigPath, readRootManifest, } from "./shared.js";
|
|
4
|
+
import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjectDirs, homePath, hookConfigPath, normalizeProjectNameForCreate, readRootManifest, } from "./shared.js";
|
|
5
5
|
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
6
6
|
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
7
7
|
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
|
|
8
8
|
import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
|
|
9
9
|
import { findProjectDir } from "./project-locator.js";
|
|
10
|
-
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask } from "./data-tasks.js";
|
|
10
|
+
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
|
|
11
|
+
import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, resolveProjectGithubRepo } from "./tasks-github.js";
|
|
11
12
|
import { PROJECT_HOOK_EVENTS, PROJECT_OWNERSHIP_MODES, isProjectHookEnabled, parseProjectOwnershipMode, readProjectConfig, writeProjectConfig, writeProjectHookConfig, } from "./project-config.js";
|
|
12
13
|
import { addFinding, removeFinding } from "./core-finding.js";
|
|
13
|
-
import { supersedeFinding, retractFinding } from "./finding-lifecycle.js";
|
|
14
|
+
import { supersedeFinding, retractFinding, resolveFindingContradiction } from "./finding-lifecycle.js";
|
|
14
15
|
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES } from "./hooks.js";
|
|
15
16
|
import { runtimeFile } from "./shared.js";
|
|
16
17
|
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
@@ -566,6 +567,10 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
566
567
|
console.log(" phren projects configure <name> Update per-project enrollment settings");
|
|
567
568
|
console.log(" flags: --ownership=<mode> --hooks=on|off");
|
|
568
569
|
console.log(" phren projects remove <name> Remove a project (asks for confirmation)");
|
|
570
|
+
console.log(" phren projects export <name> Export project data as JSON to stdout");
|
|
571
|
+
console.log(" phren projects import <file> Import project from a JSON file");
|
|
572
|
+
console.log(" phren projects archive <name> Archive a project (removes from active index)");
|
|
573
|
+
console.log(" phren projects unarchive <name> Restore an archived project");
|
|
569
574
|
return;
|
|
570
575
|
}
|
|
571
576
|
return handleProjectsList(profile);
|
|
@@ -630,8 +635,203 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
630
635
|
console.log(`Updated ${name}: ${updates.join(", ")}`);
|
|
631
636
|
return;
|
|
632
637
|
}
|
|
638
|
+
if (subcommand === "export") {
|
|
639
|
+
const name = args[1];
|
|
640
|
+
if (!name) {
|
|
641
|
+
console.error("Usage: phren projects export <name>");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
if (!isValidProjectName(name)) {
|
|
645
|
+
console.error(`Invalid project name: "${name}".`);
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
const phrenPath = getPhrenPath();
|
|
649
|
+
const projectDir = path.join(phrenPath, name);
|
|
650
|
+
if (!fs.existsSync(projectDir)) {
|
|
651
|
+
console.error(`Project "${name}" not found.`);
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
const { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } = await import("./data-access.js");
|
|
655
|
+
const exported = { project: name, exportedAt: new Date().toISOString(), version: 1 };
|
|
656
|
+
const summaryPath = path.join(projectDir, "summary.md");
|
|
657
|
+
if (fs.existsSync(summaryPath))
|
|
658
|
+
exported.summary = fs.readFileSync(summaryPath, "utf8");
|
|
659
|
+
const learningsResult = readFindings(phrenPath, name);
|
|
660
|
+
if (learningsResult.ok)
|
|
661
|
+
exported.learnings = learningsResult.data;
|
|
662
|
+
const findingsPath = path.join(projectDir, "FINDINGS.md");
|
|
663
|
+
if (fs.existsSync(findingsPath))
|
|
664
|
+
exported.findingsRaw = fs.readFileSync(findingsPath, "utf8");
|
|
665
|
+
const taskResult = readTasks(phrenPath, name);
|
|
666
|
+
if (taskResult.ok) {
|
|
667
|
+
exported.task = taskResult.data.items;
|
|
668
|
+
const taskRawPath = resolveTaskFilePath(phrenPath, name);
|
|
669
|
+
if (taskRawPath && fs.existsSync(taskRawPath))
|
|
670
|
+
exported.taskRaw = fs.readFileSync(taskRawPath, "utf8");
|
|
671
|
+
}
|
|
672
|
+
const claudePath = path.join(projectDir, "CLAUDE.md");
|
|
673
|
+
if (fs.existsSync(claudePath))
|
|
674
|
+
exported.claudeMd = fs.readFileSync(claudePath, "utf8");
|
|
675
|
+
process.stdout.write(JSON.stringify(exported, null, 2) + "\n");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (subcommand === "import") {
|
|
679
|
+
const filePath = args[1];
|
|
680
|
+
if (!filePath) {
|
|
681
|
+
console.error("Usage: phren projects import <file>");
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
const resolvedPath = path.resolve(expandHomePath(filePath));
|
|
685
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
686
|
+
console.error(`File not found: ${resolvedPath}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
let rawData;
|
|
690
|
+
try {
|
|
691
|
+
rawData = fs.readFileSync(resolvedPath, "utf8");
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
console.error(`Failed to read file: ${errorMessage(err)}`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
let decoded;
|
|
698
|
+
try {
|
|
699
|
+
decoded = JSON.parse(rawData);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
console.error("Invalid JSON in file.");
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
if (!decoded || typeof decoded !== "object" || !decoded.project) {
|
|
706
|
+
console.error("Invalid import payload: missing project field.");
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const { TASKS_FILENAME } = await import("./data-access.js");
|
|
710
|
+
const phrenPath = getPhrenPath();
|
|
711
|
+
const projectName = normalizeProjectNameForCreate(String(decoded.project));
|
|
712
|
+
if (!isValidProjectName(projectName)) {
|
|
713
|
+
console.error(`Invalid project name: "${decoded.project}".`);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
const existingProject = findProjectNameCaseInsensitive(phrenPath, projectName);
|
|
717
|
+
if (existingProject && existingProject !== projectName) {
|
|
718
|
+
console.error(`Project "${existingProject}" already exists with different casing. Refusing to import "${projectName}".`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const projectDir = path.join(phrenPath, projectName);
|
|
722
|
+
if (fs.existsSync(projectDir)) {
|
|
723
|
+
console.error(`Project "${projectName}" already exists. Remove it first or use the MCP tool with overwrite:true.`);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
const imported = [];
|
|
727
|
+
const stagingRoot = fs.mkdtempSync(path.join(phrenPath, `.phren-import-${projectName}-`));
|
|
728
|
+
const stagedProjectDir = path.join(stagingRoot, projectName);
|
|
729
|
+
try {
|
|
730
|
+
fs.mkdirSync(stagedProjectDir, { recursive: true });
|
|
731
|
+
if (typeof decoded.summary === "string") {
|
|
732
|
+
fs.writeFileSync(path.join(stagedProjectDir, "summary.md"), decoded.summary);
|
|
733
|
+
imported.push("summary.md");
|
|
734
|
+
}
|
|
735
|
+
if (typeof decoded.claudeMd === "string") {
|
|
736
|
+
fs.writeFileSync(path.join(stagedProjectDir, "CLAUDE.md"), decoded.claudeMd);
|
|
737
|
+
imported.push("CLAUDE.md");
|
|
738
|
+
}
|
|
739
|
+
if (typeof decoded.findingsRaw === "string") {
|
|
740
|
+
fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), decoded.findingsRaw);
|
|
741
|
+
imported.push("FINDINGS.md");
|
|
742
|
+
}
|
|
743
|
+
else if (Array.isArray(decoded.learnings) && decoded.learnings.length > 0) {
|
|
744
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
745
|
+
const lines = [`# ${projectName} Findings`, "", `## ${date}`, ""];
|
|
746
|
+
for (const item of decoded.learnings) {
|
|
747
|
+
if (item.text)
|
|
748
|
+
lines.push(`- ${item.text}`);
|
|
749
|
+
}
|
|
750
|
+
lines.push("");
|
|
751
|
+
fs.writeFileSync(path.join(stagedProjectDir, "FINDINGS.md"), lines.join("\n"));
|
|
752
|
+
imported.push("FINDINGS.md");
|
|
753
|
+
}
|
|
754
|
+
if (typeof decoded.taskRaw === "string") {
|
|
755
|
+
fs.writeFileSync(path.join(stagedProjectDir, TASKS_FILENAME), decoded.taskRaw);
|
|
756
|
+
imported.push(TASKS_FILENAME);
|
|
757
|
+
}
|
|
758
|
+
fs.renameSync(stagedProjectDir, projectDir);
|
|
759
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
760
|
+
console.log(`Imported project "${projectName}": ${imported.join(", ") || "(no files)"}`);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
try {
|
|
764
|
+
fs.rmSync(stagingRoot, { recursive: true, force: true });
|
|
765
|
+
}
|
|
766
|
+
catch { /* best-effort */ }
|
|
767
|
+
console.error(`Import failed: ${errorMessage(err)}`);
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (subcommand === "archive" || subcommand === "unarchive") {
|
|
773
|
+
const name = args[1];
|
|
774
|
+
if (!name) {
|
|
775
|
+
console.error(`Usage: phren projects ${subcommand} <name>`);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
if (!isValidProjectName(name)) {
|
|
779
|
+
console.error(`Invalid project name: "${name}".`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
const phrenPath = getPhrenPath();
|
|
783
|
+
const projectDir = path.join(phrenPath, name);
|
|
784
|
+
const archiveDir = path.join(phrenPath, `${name}.archived`);
|
|
785
|
+
if (subcommand === "archive") {
|
|
786
|
+
if (!fs.existsSync(projectDir)) {
|
|
787
|
+
console.error(`Project "${name}" not found.`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
if (fs.existsSync(archiveDir)) {
|
|
791
|
+
console.error(`Archive "${name}.archived" already exists. Unarchive or remove it first.`);
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
fs.renameSync(projectDir, archiveDir);
|
|
796
|
+
console.log(`Archived project "${name}". Data preserved at ${archiveDir}.`);
|
|
797
|
+
console.log("Note: the search index will be updated on next search.");
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
console.error(`Archive failed: ${errorMessage(err)}`);
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
if (fs.existsSync(projectDir)) {
|
|
806
|
+
console.error(`Project "${name}" already exists as an active project.`);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
if (!fs.existsSync(archiveDir)) {
|
|
810
|
+
const available = fs.readdirSync(phrenPath)
|
|
811
|
+
.filter((e) => e.endsWith(".archived"))
|
|
812
|
+
.map((e) => e.replace(/\.archived$/, ""));
|
|
813
|
+
if (available.length > 0) {
|
|
814
|
+
console.error(`No archive found for "${name}". Available archives: ${available.join(", ")}`);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
console.error(`No archive found for "${name}".`);
|
|
818
|
+
}
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
fs.renameSync(archiveDir, projectDir);
|
|
823
|
+
console.log(`Unarchived project "${name}". It is now active again.`);
|
|
824
|
+
console.log("Note: the search index will be updated on next search.");
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
console.error(`Unarchive failed: ${errorMessage(err)}`);
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
633
833
|
console.error(`Unknown subcommand: ${subcommand}`);
|
|
634
|
-
console.error("Usage: phren projects [list|configure|remove]");
|
|
834
|
+
console.error("Usage: phren projects [list|configure|remove|export|import|archive|unarchive]");
|
|
635
835
|
process.exit(1);
|
|
636
836
|
}
|
|
637
837
|
function handleProjectsList(profile) {
|
|
@@ -717,6 +917,13 @@ function printTaskUsage() {
|
|
|
717
917
|
console.log("Usage:");
|
|
718
918
|
console.log(' phren task add <project> "<text>"');
|
|
719
919
|
console.log(' phren task complete <project> "<text>"');
|
|
920
|
+
console.log(' phren task remove <project> "<text>"');
|
|
921
|
+
console.log(' phren task next [project]');
|
|
922
|
+
console.log(' phren task promote <project> "<text>" [--active]');
|
|
923
|
+
console.log(' phren task tidy [project] [--keep=<n>] [--dry-run]');
|
|
924
|
+
console.log(' phren task link <project> "<text>" --issue <number> [--url <url>]');
|
|
925
|
+
console.log(' phren task link <project> "<text>" --unlink');
|
|
926
|
+
console.log(' phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
|
|
720
927
|
console.log(' phren task update <project> "<text>" [--priority=high|medium|low] [--section=Active|Queue|Done] [--context="..."]');
|
|
721
928
|
console.log(' phren task pin <project> "<text>"');
|
|
722
929
|
console.log(' phren task reorder <project> "<text>" --rank=<n>');
|
|
@@ -843,6 +1050,231 @@ export async function handleTaskNamespace(args) {
|
|
|
843
1050
|
console.log(result.data);
|
|
844
1051
|
return;
|
|
845
1052
|
}
|
|
1053
|
+
if (subcommand === "remove") {
|
|
1054
|
+
const project = args[1];
|
|
1055
|
+
const match = args.slice(2).join(" ");
|
|
1056
|
+
if (!project || !match) {
|
|
1057
|
+
console.error('Usage: phren task remove <project> "<text>"');
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
const result = removeTask(getPhrenPath(), project, match);
|
|
1061
|
+
if (!result.ok) {
|
|
1062
|
+
console.error(result.error);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
console.log(result.data);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (subcommand === "next") {
|
|
1069
|
+
const project = args[1];
|
|
1070
|
+
if (!project) {
|
|
1071
|
+
console.error("Usage: phren task next <project>");
|
|
1072
|
+
process.exit(1);
|
|
1073
|
+
}
|
|
1074
|
+
const result = workNextTask(getPhrenPath(), project);
|
|
1075
|
+
if (!result.ok) {
|
|
1076
|
+
console.error(result.error);
|
|
1077
|
+
process.exit(1);
|
|
1078
|
+
}
|
|
1079
|
+
console.log(result.data);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
if (subcommand === "promote") {
|
|
1083
|
+
const project = args[1];
|
|
1084
|
+
if (!project) {
|
|
1085
|
+
printTaskUsage();
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
const positional = [];
|
|
1089
|
+
let moveToActive = false;
|
|
1090
|
+
for (const arg of args.slice(2)) {
|
|
1091
|
+
if (arg === "--active") {
|
|
1092
|
+
moveToActive = true;
|
|
1093
|
+
}
|
|
1094
|
+
else if (!arg.startsWith("--")) {
|
|
1095
|
+
positional.push(arg);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const match = positional.join(" ");
|
|
1099
|
+
if (!match) {
|
|
1100
|
+
console.error('Usage: phren task promote <project> "<text>" [--active]');
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
const result = promoteTask(getPhrenPath(), project, match, moveToActive);
|
|
1104
|
+
if (!result.ok) {
|
|
1105
|
+
console.error(result.error);
|
|
1106
|
+
process.exit(1);
|
|
1107
|
+
}
|
|
1108
|
+
console.log(`Promoted task "${result.data.line}" in ${project}${moveToActive ? " (moved to Active)" : ""}.`);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (subcommand === "tidy") {
|
|
1112
|
+
const project = args[1];
|
|
1113
|
+
if (!project) {
|
|
1114
|
+
console.error("Usage: phren task tidy <project> [--keep=<n>] [--dry-run]");
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
let keep = 30;
|
|
1118
|
+
let dryRun = false;
|
|
1119
|
+
for (const arg of args.slice(2)) {
|
|
1120
|
+
if (arg.startsWith("--keep=")) {
|
|
1121
|
+
const n = Number.parseInt(arg.slice("--keep=".length), 10);
|
|
1122
|
+
if (Number.isFinite(n) && n > 0)
|
|
1123
|
+
keep = n;
|
|
1124
|
+
}
|
|
1125
|
+
else if (arg === "--dry-run") {
|
|
1126
|
+
dryRun = true;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
const result = tidyDoneTasks(getPhrenPath(), project, keep, dryRun);
|
|
1130
|
+
if (!result.ok) {
|
|
1131
|
+
console.error(result.error);
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
console.log(result.data);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (subcommand === "link") {
|
|
1138
|
+
const project = args[1];
|
|
1139
|
+
if (!project) {
|
|
1140
|
+
printTaskUsage();
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
const positional = [];
|
|
1144
|
+
let issueArg;
|
|
1145
|
+
let urlArg;
|
|
1146
|
+
let unlink = false;
|
|
1147
|
+
const rest = args.slice(2);
|
|
1148
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1149
|
+
const arg = rest[i];
|
|
1150
|
+
if (arg === "--issue" || arg === "-i") {
|
|
1151
|
+
issueArg = rest[++i];
|
|
1152
|
+
}
|
|
1153
|
+
else if (arg.startsWith("--issue=")) {
|
|
1154
|
+
issueArg = arg.slice("--issue=".length);
|
|
1155
|
+
}
|
|
1156
|
+
else if (arg === "--url") {
|
|
1157
|
+
urlArg = rest[++i];
|
|
1158
|
+
}
|
|
1159
|
+
else if (arg.startsWith("--url=")) {
|
|
1160
|
+
urlArg = arg.slice("--url=".length);
|
|
1161
|
+
}
|
|
1162
|
+
else if (arg === "--unlink") {
|
|
1163
|
+
unlink = true;
|
|
1164
|
+
}
|
|
1165
|
+
else if (!arg.startsWith("--")) {
|
|
1166
|
+
positional.push(arg);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const match = positional.join(" ");
|
|
1170
|
+
if (!match) {
|
|
1171
|
+
console.error('Usage: phren task link <project> "<text>" --issue <number>');
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
if (!unlink && !issueArg && !urlArg) {
|
|
1175
|
+
console.error("Provide --issue <number> or --url <url> to link, or --unlink to remove the link.");
|
|
1176
|
+
process.exit(1);
|
|
1177
|
+
}
|
|
1178
|
+
if (urlArg) {
|
|
1179
|
+
const parsed = parseGithubIssueUrl(urlArg);
|
|
1180
|
+
if (!parsed) {
|
|
1181
|
+
console.error("--url must be a valid GitHub issue URL.");
|
|
1182
|
+
process.exit(1);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const result = linkTaskIssue(getPhrenPath(), project, match, {
|
|
1186
|
+
github_issue: issueArg,
|
|
1187
|
+
github_url: urlArg,
|
|
1188
|
+
unlink: unlink,
|
|
1189
|
+
});
|
|
1190
|
+
if (!result.ok) {
|
|
1191
|
+
console.error(result.error);
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
if (unlink) {
|
|
1195
|
+
console.log(`Removed GitHub link from ${project} task.`);
|
|
1196
|
+
}
|
|
1197
|
+
else {
|
|
1198
|
+
console.log(`Linked ${project} task to ${result.data.githubIssue ? `#${result.data.githubIssue}` : result.data.githubUrl}.`);
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (subcommand === "create-issue") {
|
|
1203
|
+
const project = args[1];
|
|
1204
|
+
if (!project) {
|
|
1205
|
+
printTaskUsage();
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
}
|
|
1208
|
+
const positional = [];
|
|
1209
|
+
let repoArg;
|
|
1210
|
+
let titleArg;
|
|
1211
|
+
let markDone = false;
|
|
1212
|
+
const rest = args.slice(2);
|
|
1213
|
+
for (let i = 0; i < rest.length; i++) {
|
|
1214
|
+
const arg = rest[i];
|
|
1215
|
+
if (arg === "--repo") {
|
|
1216
|
+
repoArg = rest[++i];
|
|
1217
|
+
}
|
|
1218
|
+
else if (arg.startsWith("--repo=")) {
|
|
1219
|
+
repoArg = arg.slice("--repo=".length);
|
|
1220
|
+
}
|
|
1221
|
+
else if (arg === "--title") {
|
|
1222
|
+
titleArg = rest[++i];
|
|
1223
|
+
}
|
|
1224
|
+
else if (arg.startsWith("--title=")) {
|
|
1225
|
+
titleArg = arg.slice("--title=".length);
|
|
1226
|
+
}
|
|
1227
|
+
else if (arg === "--done") {
|
|
1228
|
+
markDone = true;
|
|
1229
|
+
}
|
|
1230
|
+
else if (!arg.startsWith("--")) {
|
|
1231
|
+
positional.push(arg);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
const match = positional.join(" ");
|
|
1235
|
+
if (!match) {
|
|
1236
|
+
console.error('Usage: phren task create-issue <project> "<text>" [--repo <owner/name>] [--title "<title>"] [--done]');
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
const phrenPath = getPhrenPath();
|
|
1240
|
+
const resolved = resolveTaskItem(phrenPath, project, match);
|
|
1241
|
+
if (!resolved.ok) {
|
|
1242
|
+
console.error(resolved.error);
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
const targetRepo = repoArg || resolveProjectGithubRepo(phrenPath, project);
|
|
1246
|
+
if (!targetRepo) {
|
|
1247
|
+
console.error("Could not infer a GitHub repo. Provide --repo <owner/name> or add a GitHub URL to CLAUDE.md/summary.md.");
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
const created = createGithubIssueForTask({
|
|
1251
|
+
repo: targetRepo,
|
|
1252
|
+
title: titleArg?.trim() || resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
|
|
1253
|
+
body: buildTaskIssueBody(project, resolved.data),
|
|
1254
|
+
});
|
|
1255
|
+
if (!created.ok) {
|
|
1256
|
+
console.error(created.error);
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
const linked = linkTaskIssue(phrenPath, project, resolved.data.stableId ? `bid:${resolved.data.stableId}` : resolved.data.id, {
|
|
1260
|
+
github_issue: created.data.issueNumber,
|
|
1261
|
+
github_url: created.data.url,
|
|
1262
|
+
});
|
|
1263
|
+
if (!linked.ok) {
|
|
1264
|
+
console.error(linked.error);
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
if (markDone) {
|
|
1268
|
+
const completionMatch = linked.data.stableId ? `bid:${linked.data.stableId}` : linked.data.id;
|
|
1269
|
+
const completed = completeTask(phrenPath, project, completionMatch);
|
|
1270
|
+
if (!completed.ok) {
|
|
1271
|
+
console.error(completed.error);
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
846
1278
|
console.error(`Unknown task subcommand: ${subcommand}`);
|
|
847
1279
|
printTaskUsage();
|
|
848
1280
|
process.exit(1);
|
|
@@ -854,6 +1286,8 @@ function printFindingUsage() {
|
|
|
854
1286
|
console.log(' phren finding remove <project> "<text>"');
|
|
855
1287
|
console.log(' phren finding supersede <project> "<text>" --by "<newer guidance>"');
|
|
856
1288
|
console.log(' phren finding retract <project> "<text>" --reason "<reason>"');
|
|
1289
|
+
console.log(' phren finding contradictions [project]');
|
|
1290
|
+
console.log(' phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
|
|
857
1291
|
}
|
|
858
1292
|
export async function handleFindingNamespace(args) {
|
|
859
1293
|
const subcommand = args[0];
|
|
@@ -983,6 +1417,65 @@ export async function handleFindingNamespace(args) {
|
|
|
983
1417
|
console.log(`Finding retracted: "${result.data.finding}" (reason: ${result.data.reason})`);
|
|
984
1418
|
return;
|
|
985
1419
|
}
|
|
1420
|
+
if (subcommand === "contradictions") {
|
|
1421
|
+
const project = args[1];
|
|
1422
|
+
const phrenPath = getPhrenPath();
|
|
1423
|
+
const RESERVED_DIRS = new Set(["global", ".runtime", ".sessions", ".governance"]);
|
|
1424
|
+
const { readFindings } = await import("./data-access.js");
|
|
1425
|
+
const projects = project
|
|
1426
|
+
? [project]
|
|
1427
|
+
: fs.readdirSync(phrenPath, { withFileTypes: true })
|
|
1428
|
+
.filter((entry) => entry.isDirectory() && !RESERVED_DIRS.has(entry.name) && isValidProjectName(entry.name))
|
|
1429
|
+
.map((entry) => entry.name);
|
|
1430
|
+
const contradictions = [];
|
|
1431
|
+
for (const p of projects) {
|
|
1432
|
+
const result = readFindings(phrenPath, p);
|
|
1433
|
+
if (!result.ok)
|
|
1434
|
+
continue;
|
|
1435
|
+
for (const finding of result.data) {
|
|
1436
|
+
if (finding.status !== "contradicted")
|
|
1437
|
+
continue;
|
|
1438
|
+
contradictions.push({ project: p, id: finding.id, text: finding.text, date: finding.date, status_ref: finding.status_ref });
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (!contradictions.length) {
|
|
1442
|
+
console.log("No unresolved contradictions found.");
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
console.log(`${contradictions.length} unresolved contradiction(s):\n`);
|
|
1446
|
+
for (const c of contradictions) {
|
|
1447
|
+
console.log(`[${c.project}] ${c.date} ${c.id}`);
|
|
1448
|
+
console.log(` ${c.text}`);
|
|
1449
|
+
if (c.status_ref)
|
|
1450
|
+
console.log(` contradicts: ${c.status_ref}`);
|
|
1451
|
+
console.log("");
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (subcommand === "resolve") {
|
|
1456
|
+
const project = args[1];
|
|
1457
|
+
const findingText = args[2];
|
|
1458
|
+
const otherText = args[3];
|
|
1459
|
+
const resolution = args[4];
|
|
1460
|
+
const validResolutions = ["keep_a", "keep_b", "keep_both", "retract_both"];
|
|
1461
|
+
if (!project || !findingText || !otherText || !resolution) {
|
|
1462
|
+
console.error('Usage: phren finding resolve <project> "<finding_text>" "<other_text>" <keep_a|keep_b|keep_both|retract_both>');
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
if (!validResolutions.includes(resolution)) {
|
|
1466
|
+
console.error(`Invalid resolution "${resolution}". Valid values: ${validResolutions.join(", ")}`);
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
const result = resolveFindingContradiction(getPhrenPath(), project, findingText, otherText, resolution);
|
|
1470
|
+
if (!result.ok) {
|
|
1471
|
+
console.error(result.error);
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
}
|
|
1474
|
+
console.log(`Resolved contradiction in "${project}" with "${resolution}".`);
|
|
1475
|
+
console.log(` finding_a: ${result.data.finding_a.text} → ${result.data.finding_a.status}`);
|
|
1476
|
+
console.log(` finding_b: ${result.data.finding_b.text} → ${result.data.finding_b.status}`);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
986
1479
|
console.error(`Unknown finding subcommand: ${subcommand}`);
|
|
987
1480
|
printFindingUsage();
|
|
988
1481
|
process.exit(1);
|
package/mcp/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowP
|
|
|
9
9
|
import { parseSearchArgs } from "./cli-search.js";
|
|
10
10
|
import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handleTaskNamespace, } from "./cli-namespaces.js";
|
|
11
11
|
import { handleTaskView, handleSessionsView, handleQuickstart, handleDebugInjection, handleInspectIndex, } from "./cli-ops.js";
|
|
12
|
-
import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleSearch, handleShell, handleStatus, handleUpdate, } from "./cli-actions.js";
|
|
12
|
+
import { handleAddFinding, handleDoctor, handleFragmentSearch, handleMemoryUi, handlePinCanonical, handleQualityFeedback, handleRelatedDocs, handleReview, handleConsolidationStatus, handleSessionContext, handleSearch, handleShell, handleStatus, handleUpdate, } from "./cli-actions.js";
|
|
13
13
|
import { handleGraphNamespace } from "./cli-graph.js";
|
|
14
14
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
15
15
|
// ── CLI router ───────────────────────────────────────────────────────────────
|
|
@@ -101,6 +101,12 @@ export async function runCliCommand(command, args) {
|
|
|
101
101
|
return handleDetectSkills(args, getProfile());
|
|
102
102
|
case "graph":
|
|
103
103
|
return handleGraphNamespace(args);
|
|
104
|
+
case "review":
|
|
105
|
+
return handleReview(args);
|
|
106
|
+
case "consolidation-status":
|
|
107
|
+
return handleConsolidationStatus(args);
|
|
108
|
+
case "session-context":
|
|
109
|
+
return handleSessionContext();
|
|
104
110
|
default:
|
|
105
111
|
console.error(`Unknown command: ${command}`);
|
|
106
112
|
process.exit(1);
|
|
@@ -5,7 +5,7 @@ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "./shared.js";
|
|
|
5
5
|
import { errorMessage, runGitOrThrow } from "./utils.js";
|
|
6
6
|
import { findingIdFromLine } from "./finding-impact.js";
|
|
7
7
|
import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./content-metadata.js";
|
|
8
|
-
import { FINDING_TYPE_DECAY, extractFindingType
|
|
8
|
+
import { FINDING_TYPE_DECAY, extractFindingType } from "./finding-lifecycle.js";
|
|
9
9
|
export const FINDING_PROVENANCE_SOURCES = [
|
|
10
10
|
"human",
|
|
11
11
|
"agent",
|
|
@@ -294,7 +294,6 @@ export function filterTrustedFindingsDetailed(content, opts) {
|
|
|
294
294
|
...(options.decay || {}),
|
|
295
295
|
};
|
|
296
296
|
const highImpactFindingIds = options.highImpactFindingIds;
|
|
297
|
-
const impactCounts = options.impactCounts;
|
|
298
297
|
const project = options.project;
|
|
299
298
|
const lines = content.split("\n");
|
|
300
299
|
const out = [];
|
|
@@ -413,29 +412,20 @@ export function filterTrustedFindingsDetailed(content, opts) {
|
|
|
413
412
|
confidence *= 0.9;
|
|
414
413
|
if (project && highImpactFindingIds?.size) {
|
|
415
414
|
const findingId = findingIdFromLine(line);
|
|
416
|
-
if (highImpactFindingIds.has(findingId))
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
|
|
428
|
-
}
|
|
415
|
+
if (highImpactFindingIds.has(findingId))
|
|
416
|
+
confidence *= 1.15;
|
|
417
|
+
}
|
|
418
|
+
// Confirmed findings decay 3x slower — recompute confidence with reduced age
|
|
419
|
+
{
|
|
420
|
+
const findingId = findingIdFromLine(line);
|
|
421
|
+
if (findingId && highImpactFindingIds?.has(findingId) && effectiveDate) {
|
|
422
|
+
const realAge = ageDaysForDate(effectiveDate);
|
|
423
|
+
if (realAge !== null) {
|
|
424
|
+
const slowedAge = Math.floor(realAge / 3);
|
|
425
|
+
confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
|
|
429
426
|
}
|
|
430
427
|
}
|
|
431
428
|
}
|
|
432
|
-
const lifecycle = parseFindingLifecycle(line);
|
|
433
|
-
if (lifecycle?.status === "superseded")
|
|
434
|
-
confidence *= 0.25;
|
|
435
|
-
if (lifecycle?.status === "retracted")
|
|
436
|
-
confidence *= 0.1;
|
|
437
|
-
if (lifecycle?.status === "contradicted")
|
|
438
|
-
confidence *= 0.4;
|
|
439
429
|
confidence = Math.max(0, Math.min(1, confidence));
|
|
440
430
|
if (confidence < minConfidence) {
|
|
441
431
|
issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "stale" });
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -8,7 +8,7 @@ import { isValidProjectName, queueFilePath, safeProjectPath, errorMessage } from
|
|
|
8
8
|
import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
|
|
9
9
|
import { parseFindingLifecycle, } from "./finding-lifecycle.js";
|
|
10
10
|
import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
|
|
11
|
-
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
11
|
+
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, removeTasks, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
12
12
|
export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
|
|
13
13
|
export { loadShellState, readRuntimeHealth, resetShellState, saveShellState, } from "./shell-state-store.js";
|
|
14
14
|
function withSafeLock(filePath, fn) {
|