@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.
@@ -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, parseFindingLifecycle } from "./finding-lifecycle.js";
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
- // Get surface count for graduated boost
418
- const surfaceCount = impactCounts?.get(findingId) ?? 3;
419
- // Log-scaled: 3→1.15x, 10→1.28x, 30→1.38x, capped at 1.4x
420
- const boost = Math.min(1.4, 1 + 0.1 * Math.log2(Math.max(3, surfaceCount)));
421
- confidence *= boost;
422
- // Decay resistance: confirmed findings decay 3x slower
423
- if (effectiveDate) {
424
- const realAge = ageDaysForDate(effectiveDate);
425
- if (realAge !== null) {
426
- const slowedAge = Math.floor(realAge / 3);
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" });
@@ -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) {