@phren/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,108 @@
1
+ import { getPhrenPath } from "./shared.js";
2
+ // Re-export from split modules so existing test imports keep working
3
+ export { detectTaskIntent, parseHookInput, applyTrustFilter, rankResults, selectSnippets, buildHookOutput, trackSessionMetrics, filterTaskByPriority, parseCitations, validateCitation, annotateStale, getProjectGlobBoost, clearProjectGlobCache, clearCitationValidCache, filterConversationInsightsForProactivity, extractToolFindings, filterToolFindingsForProactivity, } from "./cli-hooks.js";
4
+ export { scoreFindingCandidate } from "./cli-extract.js";
5
+ import { handleHookPrompt, handleHookSessionStart, handleHookStop, handleBackgroundSync, handleHookContext, handleHookTool, } from "./cli-hooks.js";
6
+ import { handleExtractMemories } from "./cli-extract.js";
7
+ import { handleGovernMemories, handlePruneMemories, handleConsolidateMemories, handleMaintain, handleBackgroundMaintenance, } from "./cli-govern.js";
8
+ import { handleConfig, handleIndexPolicy, handleRetentionPolicy, handleWorkflowPolicy, } from "./cli-config.js";
9
+ import { parseSearchArgs } from "./cli-search.js";
10
+ import { handleDetectSkills, handleFindingNamespace, handleHooksNamespace, handleProjectsNamespace, handleSkillsNamespace, handleSkillList, handleTaskNamespace, } from "./cli-namespaces.js";
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";
13
+ import { handleGraphNamespace } from "./cli-graph.js";
14
+ import { resolveRuntimeProfile } from "./runtime-profile.js";
15
+ // ── CLI router ───────────────────────────────────────────────────────────────
16
+ export async function runCliCommand(command, args) {
17
+ const getProfile = () => resolveRuntimeProfile(getPhrenPath());
18
+ switch (command) {
19
+ case "search":
20
+ {
21
+ const opts = parseSearchArgs(getPhrenPath(), args);
22
+ if (!opts)
23
+ return;
24
+ return handleSearch(opts, getProfile());
25
+ }
26
+ case "hook-prompt":
27
+ return handleHookPrompt();
28
+ case "hook-session-start":
29
+ return handleHookSessionStart();
30
+ case "hook-stop":
31
+ return handleHookStop();
32
+ case "background-sync":
33
+ return handleBackgroundSync();
34
+ case "hook-context":
35
+ return handleHookContext();
36
+ case "hook-tool":
37
+ return handleHookTool();
38
+ case "add-finding":
39
+ return handleAddFinding(args[0], args.slice(1).join(" "));
40
+ case "extract-memories":
41
+ return handleExtractMemories(args[0]);
42
+ case "govern-memories":
43
+ return handleGovernMemories(args[0]);
44
+ case "pin":
45
+ return handlePinCanonical(args[0], args.slice(1).join(" "));
46
+ case "doctor":
47
+ return handleDoctor(args);
48
+ case "status":
49
+ return handleStatus();
50
+ case "quality-feedback":
51
+ return handleQualityFeedback(args);
52
+ case "prune-memories":
53
+ return handlePruneMemories(args);
54
+ case "consolidate-memories":
55
+ return handleConsolidateMemories(args);
56
+ case "index-policy":
57
+ return handleIndexPolicy(args);
58
+ case "policy":
59
+ return handleRetentionPolicy(args);
60
+ case "workflow":
61
+ return handleWorkflowPolicy(args);
62
+ case "web-ui":
63
+ return handleMemoryUi(args);
64
+ case "shell":
65
+ return handleShell(args, getProfile());
66
+ case "update":
67
+ return handleUpdate(args);
68
+ case "config":
69
+ return handleConfig(args);
70
+ case "maintain":
71
+ return handleMaintain(args);
72
+ case "skill-list":
73
+ return handleSkillList(getProfile());
74
+ case "skills":
75
+ return handleSkillsNamespace(args, getProfile());
76
+ case "hooks":
77
+ return handleHooksNamespace(args);
78
+ case "tasks":
79
+ return handleTaskView(getProfile());
80
+ case "sessions":
81
+ return handleSessionsView(args);
82
+ case "task":
83
+ return handleTaskNamespace(args);
84
+ case "finding":
85
+ return handleFindingNamespace(args);
86
+ case "projects":
87
+ return handleProjectsNamespace(args, args[0] === "--help" || args[0] === "-h" ? "" : getProfile());
88
+ case "quickstart":
89
+ return handleQuickstart();
90
+ case "background-maintenance":
91
+ return handleBackgroundMaintenance(args[0]);
92
+ case "debug-injection":
93
+ return handleDebugInjection(args, getProfile());
94
+ case "inspect-index":
95
+ return handleInspectIndex(args, getProfile());
96
+ case "search-fragments":
97
+ return handleFragmentSearch(args, getProfile());
98
+ case "related-docs":
99
+ return handleRelatedDocs(args, getProfile());
100
+ case "detect-skills":
101
+ return handleDetectSkills(args, getProfile());
102
+ case "graph":
103
+ return handleGraphNamespace(args);
104
+ default:
105
+ console.error(`Unknown command: ${command}`);
106
+ process.exit(1);
107
+ }
108
+ }
@@ -0,0 +1,278 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as crypto from "crypto";
4
+ import { debugLog, runtimeFile, phrenOk, phrenErr, PhrenError, appendAuditLog, tryUnlink } from "./shared.js";
5
+ import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
6
+ import { withFileLock } from "./shared-governance.js";
7
+ import { appendArchivedEntriesToTopicDoc, classifyTopicForText, readProjectTopics, topicReferencePath } from "./project-topics.js";
8
+ import { isCitationLine, isArchiveStart, isArchiveEnd, stripComments } from "./content-metadata.js";
9
+ /**
10
+ * Count active (non-archived) finding entries in FINDINGS.md content.
11
+ * Entries inside archive blocks are considered archived.
12
+ * Supports structured archive markers and HTML details blocks.
13
+ */
14
+ export function countActiveFindings(content) {
15
+ let inArchive = false;
16
+ let count = 0;
17
+ for (const line of content.split("\n")) {
18
+ if (isArchiveStart(line)) {
19
+ inArchive = true;
20
+ continue;
21
+ }
22
+ if (isArchiveEnd(line)) {
23
+ inArchive = false;
24
+ continue;
25
+ }
26
+ if (!inArchive && line.startsWith("- "))
27
+ count++;
28
+ }
29
+ return count;
30
+ }
31
+ /**
32
+ * Parse active (non-archived) entries from FINDINGS.md, oldest first.
33
+ */
34
+ function parseActiveEntries(content) {
35
+ const lines = content.split("\n");
36
+ const entries = [];
37
+ let currentDate = "";
38
+ let inArchive = false;
39
+ for (let i = 0; i < lines.length; i++) {
40
+ const line = lines[i];
41
+ if (isArchiveStart(line)) {
42
+ inArchive = true;
43
+ continue;
44
+ }
45
+ if (isArchiveEnd(line)) {
46
+ inArchive = false;
47
+ continue;
48
+ }
49
+ if (inArchive)
50
+ continue;
51
+ const heading = line.match(/^## (\d{4}-\d{2}-\d{2})$/);
52
+ if (heading) {
53
+ currentDate = heading[1];
54
+ continue;
55
+ }
56
+ if (line.startsWith("- ") && currentDate) {
57
+ const next = lines[i + 1] || "";
58
+ const hasCitation = isCitationLine(next);
59
+ entries.push({
60
+ date: currentDate,
61
+ bullet: line,
62
+ citation: hasCitation ? next : undefined,
63
+ lineIndex: i,
64
+ });
65
+ if (hasCitation)
66
+ i++;
67
+ }
68
+ }
69
+ // Sort oldest first: earliest date first; within the same date, higher
70
+ // lineIndex = earlier in the file (newest findings are prepended at top).
71
+ // Q25: use descending lineIndex within the same day so that when we slice
72
+ // `toArchive = entries.slice(0, N)` we archive the truly oldest entries
73
+ // (lowest in the file = largest lineIndex for that date).
74
+ entries.sort((a, b) => a.date.localeCompare(b.date) || b.lineIndex - a.lineIndex);
75
+ return entries;
76
+ }
77
+ /**
78
+ * Check whether a bullet already exists in a reference file (already archived).
79
+ */
80
+ function isAlreadyArchived(referenceDir, bullet) {
81
+ if (!fs.existsSync(referenceDir))
82
+ return false;
83
+ const normalizedBullet = stripComments(bullet).replace(/^-\s+/, "").trim().toLowerCase();
84
+ if (!normalizedBullet)
85
+ return false;
86
+ try {
87
+ const stack = [referenceDir];
88
+ while (stack.length > 0) {
89
+ const current = stack.pop();
90
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
91
+ const fullPath = path.join(current, entry.name);
92
+ if (entry.isDirectory()) {
93
+ stack.push(fullPath);
94
+ continue;
95
+ }
96
+ if (!entry.isFile() || !entry.name.endsWith(".md"))
97
+ continue;
98
+ const content = fs.readFileSync(fullPath, "utf8");
99
+ for (const line of content.split("\n")) {
100
+ if (!line.startsWith("- "))
101
+ continue;
102
+ const normalizedLine = stripComments(line).replace(/^-\s+/, "").trim().toLowerCase();
103
+ if (normalizedLine === normalizedBullet)
104
+ return true;
105
+ }
106
+ }
107
+ }
108
+ }
109
+ catch (err) {
110
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
111
+ process.stderr.write(`[phren] isDuplicateInReference: ${errorMessage(err)}\n`);
112
+ }
113
+ return false;
114
+ }
115
+ /**
116
+ * Archive the oldest entries from FINDINGS.md into reference/{topic}.md files.
117
+ * Keeps `keepCount` most recent entries, archives the rest grouped by topic.
118
+ * Returns the number of entries archived.
119
+ */
120
+ export function autoArchiveToReference(phrenPath, project, keepCount) {
121
+ if (!isValidProjectName(project))
122
+ return phrenErr(`Invalid project name: "${project}".`, PhrenError.INVALID_PROJECT_NAME);
123
+ const resolvedDir = safeProjectPath(phrenPath, project);
124
+ if (!resolvedDir || !fs.existsSync(resolvedDir))
125
+ return phrenErr(`Project "${project}" not found in phren.`, PhrenError.PROJECT_NOT_FOUND);
126
+ const learningsPath = path.join(resolvedDir, "FINDINGS.md");
127
+ if (!fs.existsSync(learningsPath))
128
+ return phrenOk(0);
129
+ // Consolidation lock to prevent concurrent runs for the same project (atomic create via wx flag).
130
+ // Use a project-specific lock so consolidating multiple projects in parallel is allowed.
131
+ const STALE_LOCK_MS = 600_000; // 10 min
132
+ const lockFile = runtimeFile(phrenPath, `consolidation-${project}.lock`);
133
+ try {
134
+ fs.writeFileSync(lockFile, String(Date.now()), { flag: "wx" });
135
+ }
136
+ catch (e) {
137
+ if (e.code === "EEXIST") {
138
+ try {
139
+ const stat = fs.statSync(lockFile);
140
+ if (Date.now() - stat.mtimeMs < STALE_LOCK_MS) {
141
+ return phrenErr("Consolidation already running", PhrenError.LOCK_TIMEOUT);
142
+ }
143
+ // Stale lock: delete then re-create atomically with wx.
144
+ tryUnlink(lockFile);
145
+ // Re-attempt atomic create. If EEXIST, another thread won the race.
146
+ try {
147
+ fs.writeFileSync(lockFile, String(Date.now()), { flag: "wx" });
148
+ }
149
+ catch (wxErr) {
150
+ if (wxErr.code === "EEXIST")
151
+ return phrenErr("Consolidation already running", PhrenError.LOCK_TIMEOUT);
152
+ throw wxErr;
153
+ }
154
+ }
155
+ catch {
156
+ return phrenErr("Consolidation already running", PhrenError.LOCK_TIMEOUT);
157
+ }
158
+ }
159
+ else {
160
+ throw e;
161
+ }
162
+ }
163
+ // Q11: Hold the per-file lock on FINDINGS.md for the entire read-modify-write
164
+ // cycle so finding writers and the archive pass see a consistent file.
165
+ try {
166
+ return withFileLock(learningsPath, () => {
167
+ const content = fs.readFileSync(learningsPath, "utf8");
168
+ const entries = parseActiveEntries(content);
169
+ if (entries.length <= keepCount)
170
+ return phrenOk(0);
171
+ const toArchive = entries.slice(0, entries.length - keepCount);
172
+ // Guard: skip entries already present in reference tier (prevent double-archive)
173
+ const referenceDir = path.join(resolvedDir, "reference");
174
+ const { topics } = readProjectTopics(phrenPath, project);
175
+ const today = new Date().toISOString().slice(0, 10);
176
+ const actuallyArchived = [];
177
+ for (const entry of toArchive) {
178
+ if (isAlreadyArchived(referenceDir, entry.bullet)) {
179
+ debugLog(`auto_archive: skipping already-archived entry: "${entry.bullet.slice(0, 60)}"`);
180
+ continue;
181
+ }
182
+ actuallyArchived.push(entry);
183
+ }
184
+ // Group archived entries by topic
185
+ const byTopic = new Map();
186
+ for (const entry of actuallyArchived) {
187
+ const topic = classifyTopicForText(entry.bullet, topics);
188
+ const bucket = byTopic.get(topic.slug) ?? [];
189
+ bucket.push({ date: today, bullet: entry.bullet, citation: entry.citation });
190
+ byTopic.set(topic.slug, bucket);
191
+ }
192
+ // Write to reference/topics/{topic}.md (atomic rename per file)
193
+ fs.mkdirSync(referenceDir, { recursive: true });
194
+ const successfulTopics = new Set();
195
+ for (const [topicSlug, topicEntries] of byTopic) {
196
+ const filePath = topicReferencePath(phrenPath, project, topicSlug);
197
+ const topic = topics.find((item) => item.slug === topicSlug) ?? topics.find((item) => item.slug === "general");
198
+ if (!filePath || !topic)
199
+ continue;
200
+ try {
201
+ appendArchivedEntriesToTopicDoc(filePath, project, topic, topicEntries);
202
+ successfulTopics.add(topicSlug);
203
+ }
204
+ catch (err) {
205
+ debugLog(`auto_archive: failed to write reference file for topic "${topicSlug}": ${errorMessage(err)}`);
206
+ }
207
+ }
208
+ // Only remove entries whose topics were successfully written to reference files,
209
+ // plus entries already present in reference (safe to remove since they're already archived).
210
+ const alreadyArchivedEntries = toArchive.filter(entry => !actuallyArchived.includes(entry));
211
+ const successfullyArchived = actuallyArchived.filter(entry => successfulTopics.has(classifyTopicForText(entry.bullet, topics).slug));
212
+ const safeToRemove = [...successfullyArchived, ...alreadyArchivedEntries];
213
+ // Remove archived entries from FINDINGS.md
214
+ const lines = content.split("\n");
215
+ const archiveLineSet = new Set();
216
+ for (const entry of safeToRemove) {
217
+ archiveLineSet.add(entry.lineIndex);
218
+ if (entry.citation)
219
+ archiveLineSet.add(entry.lineIndex + 1);
220
+ }
221
+ const filtered = lines.filter((_, i) => !archiveLineSet.has(i));
222
+ // Clean up empty date sections
223
+ const cleaned = [];
224
+ for (let i = 0; i < filtered.length; i++) {
225
+ const line = filtered[i];
226
+ const isDateHeading = /^## \d{4}-\d{2}-\d{2}$/.test(line);
227
+ if (isDateHeading) {
228
+ // Check if next non-empty lines have any bullets
229
+ let hasBullets = false;
230
+ for (let j = i + 1; j < filtered.length; j++) {
231
+ const next = filtered[j].trim();
232
+ if (!next)
233
+ continue;
234
+ if (next.startsWith("## ") || next.startsWith("# "))
235
+ break;
236
+ if (next.startsWith("- ")) {
237
+ hasBullets = true;
238
+ break;
239
+ }
240
+ break;
241
+ }
242
+ if (!hasBullets)
243
+ continue;
244
+ }
245
+ cleaned.push(line);
246
+ }
247
+ // Write consolidation marker
248
+ const marker = `<!-- consolidated: ${today} -->`;
249
+ const markerIdx = cleaned.findIndex(l => l.includes("consolidated:"));
250
+ if (markerIdx >= 0) {
251
+ cleaned[markerIdx] = marker;
252
+ }
253
+ else {
254
+ // Insert after title
255
+ const titleIdx = cleaned.findIndex(l => l.startsWith("# "));
256
+ if (titleIdx >= 0) {
257
+ cleaned.splice(titleIdx + 1, 0, "", marker);
258
+ }
259
+ }
260
+ const tmpPath = learningsPath + `.tmp-${crypto.randomUUID()}`;
261
+ fs.writeFileSync(tmpPath, cleaned.join("\n"));
262
+ fs.renameSync(tmpPath, learningsPath);
263
+ const skippedCount = alreadyArchivedEntries.length;
264
+ const failedTopics = [...byTopic.keys()].filter(t => !successfulTopics.has(t));
265
+ appendAuditLog(phrenPath, "auto_archive_reference", `project=${project} archived=${successfullyArchived.length} skipped_duplicates=${skippedCount}${failedTopics.length ? ` failed_topics=${failedTopics.join(",")}` : ""} topics=${[...successfulTopics].join(",")}`);
266
+ return phrenOk(safeToRemove.length);
267
+ });
268
+ }
269
+ finally {
270
+ try {
271
+ fs.unlinkSync(lockFile);
272
+ }
273
+ catch (err) {
274
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
275
+ process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
276
+ }
277
+ }
278
+ }