@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.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- package/starter/templates/python-project/tasks.md +10 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// cli-hooks.ts — Thin orchestrator. Delegates to focused modules:
|
|
2
|
+
// shared-retrieval.ts — shared search, scoring, ranking, snippet selection
|
|
3
|
+
// cli-hooks-citations.ts — citation parsing and validation
|
|
4
|
+
// cli-hooks-session.ts — session lifecycle hooks, metrics, background maintenance
|
|
5
|
+
// cli-hooks-output.ts — hook output formatting
|
|
6
|
+
// cli-hooks-globs.ts — project glob matching
|
|
7
|
+
import { debugLog, sessionMarker, sessionsDir, getPhrenPath, } from "./shared.js";
|
|
8
|
+
import { getRetentionPolicy, getWorkflowPolicy, } from "./shared-governance.js";
|
|
9
|
+
import { buildIndex, detectProject, } from "./shared-index.js";
|
|
10
|
+
import { isProjectHookEnabled } from "./project-config.js";
|
|
11
|
+
import { checkConsolidationNeeded, } from "./shared-content.js";
|
|
12
|
+
import { buildRobustFtsQuery, extractKeywordEntries, isFeatureEnabled, clampInt, errorMessage, loadSynonymMap, learnSynonym, STOP_WORDS, } from "./utils.js";
|
|
13
|
+
import { getHooksEnabledPreference } from "./init.js";
|
|
14
|
+
import { isToolHookEnabled } from "./hooks.js";
|
|
15
|
+
import { handleExtractMemories } from "./cli-extract.js";
|
|
16
|
+
import { appendAuditLog } from "./shared.js";
|
|
17
|
+
import { updateRuntimeHealth } from "./shared-governance.js";
|
|
18
|
+
import { getProactivityLevelForTask, getProactivityLevelForFindings } from "./proactivity.js";
|
|
19
|
+
import { FINDING_SENSITIVITY_CONFIG } from "./cli-config.js";
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
// ── Re-exports from focused modules ─────────────────────────────────────────
|
|
22
|
+
// Citations
|
|
23
|
+
export { parseCitations, validateCitation, annotateStale, clearCitationValidCache, } from "./cli-hooks-citations.js";
|
|
24
|
+
// Globs
|
|
25
|
+
export { getProjectGlobBoost, clearProjectGlobCache, } from "./cli-hooks-globs.js";
|
|
26
|
+
// Retrieval
|
|
27
|
+
export { detectTaskIntent, filterTaskByPriority, searchDocuments, applyTrustFilter, rankResults, selectSnippets, } from "./shared-retrieval.js";
|
|
28
|
+
// Output
|
|
29
|
+
export { buildHookOutput, } from "./cli-hooks-output.js";
|
|
30
|
+
// Session
|
|
31
|
+
export { handleHookSessionStart, handleHookStop, handleBackgroundSync, handleHookContext, handleHookTool, trackSessionMetrics, filterConversationInsightsForProactivity, extractToolFindings, filterToolFindingsForProactivity, resolveSubprocessArgs, } from "./cli-hooks-session.js";
|
|
32
|
+
// ── Imports for the orchestrator ─────────────────────────────────────────────
|
|
33
|
+
import { searchDocumentsAsync, applyTrustFilter, rankResults, selectSnippets, detectTaskIntent, } from "./shared-retrieval.js";
|
|
34
|
+
import { buildHookOutput } from "./cli-hooks-output.js";
|
|
35
|
+
import { getGitContext, trackSessionMetrics, } from "./cli-hooks-session.js";
|
|
36
|
+
import { approximateTokens } from "./shared-retrieval.js";
|
|
37
|
+
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
38
|
+
import { handleTaskPromptLifecycle } from "./task-lifecycle.js";
|
|
39
|
+
function synonymTermKnown(term, map) {
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(map, term))
|
|
41
|
+
return true;
|
|
42
|
+
for (const values of Object.values(map)) {
|
|
43
|
+
if (values.includes(term))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function termAppearsInText(term, text) {
|
|
49
|
+
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\s+/g, "\\s+");
|
|
50
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
51
|
+
return pattern.test(text);
|
|
52
|
+
}
|
|
53
|
+
function autoLearnQuerySynonyms(phrenPath, project, keywordEntries, rows) {
|
|
54
|
+
if (!project)
|
|
55
|
+
return;
|
|
56
|
+
const synonymMap = loadSynonymMap(project, phrenPath);
|
|
57
|
+
const knownTerms = new Set([
|
|
58
|
+
...Object.keys(synonymMap),
|
|
59
|
+
...Object.values(synonymMap).flat(),
|
|
60
|
+
]);
|
|
61
|
+
const queryTerms = [...new Set(keywordEntries
|
|
62
|
+
.map((item) => item.trim().toLowerCase())
|
|
63
|
+
.filter((item) => item.length > 2 && !STOP_WORDS.has(item)))];
|
|
64
|
+
const unknownTerms = queryTerms.filter((term) => !synonymTermKnown(term, synonymMap));
|
|
65
|
+
if (unknownTerms.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
const corpus = rows
|
|
68
|
+
.slice(0, 8)
|
|
69
|
+
.map((row) => row.content.slice(0, 6000))
|
|
70
|
+
.join("\n")
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
if (!corpus.trim())
|
|
73
|
+
return;
|
|
74
|
+
const learned = [];
|
|
75
|
+
for (const unknown of unknownTerms.slice(0, 3)) {
|
|
76
|
+
const related = [...knownTerms]
|
|
77
|
+
.filter((candidate) => candidate.length > 2
|
|
78
|
+
&& candidate !== unknown
|
|
79
|
+
&& !STOP_WORDS.has(candidate)
|
|
80
|
+
&& !queryTerms.includes(candidate)
|
|
81
|
+
&& termAppearsInText(candidate, corpus))
|
|
82
|
+
.slice(0, 4);
|
|
83
|
+
if (related.length === 0)
|
|
84
|
+
continue;
|
|
85
|
+
try {
|
|
86
|
+
learnSynonym(phrenPath, project, unknown, related);
|
|
87
|
+
learned.push({ term: unknown, related });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
debugLog(`hook-prompt synonym-learn failed for "${unknown}": ${errorMessage(err)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (learned.length > 0) {
|
|
94
|
+
const details = learned.map((entry) => `${entry.term}->${entry.related.join(",")}`).join("; ");
|
|
95
|
+
debugLog(`hook-prompt learned synonyms project=${project} ${details}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function readStdin() {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const chunks = [];
|
|
101
|
+
process.stdin.on("data", (chunk) => chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
|
102
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
103
|
+
process.stdin.on("error", reject);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export function parseHookInput(raw) {
|
|
107
|
+
try {
|
|
108
|
+
const data = JSON.parse(raw);
|
|
109
|
+
const prompt = data.prompt || "";
|
|
110
|
+
if (!prompt.trim())
|
|
111
|
+
return null;
|
|
112
|
+
return { prompt, cwd: data.cwd, sessionId: data.session_id };
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
debugLog(`parseHookInput: failed to parse hook JSON: ${errorMessage(err)}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ── handleHookPrompt: orchestrator using extracted stages ────────────────────
|
|
120
|
+
export async function handleHookPrompt() {
|
|
121
|
+
const profile = resolveRuntimeProfile(getPhrenPath());
|
|
122
|
+
const stage = { indexMs: 0, searchMs: 0, trustMs: 0, rankMs: 0, selectMs: 0 };
|
|
123
|
+
let raw = "";
|
|
124
|
+
try {
|
|
125
|
+
raw = await readStdin();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (process.env.PHREN_DEBUG)
|
|
129
|
+
process.stderr.write(`[phren] hookPrompt stdinRead: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
const input = parseHookInput(raw);
|
|
133
|
+
if (!input)
|
|
134
|
+
process.exit(0);
|
|
135
|
+
const { prompt, cwd, sessionId } = input;
|
|
136
|
+
if (!getHooksEnabledPreference(getPhrenPath())) {
|
|
137
|
+
appendAuditLog(getPhrenPath(), "hook_prompt", "status=disabled");
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
// Check per-tool hook preference (PHREN_HOOK_TOOL is set by session wrappers;
|
|
141
|
+
// Claude hooks always run as "claude" from settings.json)
|
|
142
|
+
const hookTool = process.env.PHREN_HOOK_TOOL || "claude";
|
|
143
|
+
if (!isToolHookEnabled(getPhrenPath(), hookTool)) {
|
|
144
|
+
appendAuditLog(getPhrenPath(), "hook_prompt", `status=tool_disabled tool=${hookTool}`);
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
updateRuntimeHealth(getPhrenPath(), { lastPromptAt: new Date().toISOString() });
|
|
148
|
+
const keywordEntries = extractKeywordEntries(prompt);
|
|
149
|
+
const keywords = keywordEntries.join(" ");
|
|
150
|
+
if (!keywords)
|
|
151
|
+
process.exit(0);
|
|
152
|
+
debugLog(`hook-prompt keywords: "${keywords}"`);
|
|
153
|
+
const tIndex0 = Date.now();
|
|
154
|
+
const db = await buildIndex(getPhrenPath(), profile);
|
|
155
|
+
stage.indexMs = Date.now() - tIndex0;
|
|
156
|
+
const gitCtx = getGitContext(cwd);
|
|
157
|
+
const intent = detectTaskIntent(prompt);
|
|
158
|
+
const detectedProject = cwd ? detectProject(getPhrenPath(), cwd, profile) : null;
|
|
159
|
+
if (detectedProject)
|
|
160
|
+
debugLog(`Detected project: ${detectedProject}`);
|
|
161
|
+
if (!isProjectHookEnabled(getPhrenPath(), detectedProject, "UserPromptSubmit")) {
|
|
162
|
+
appendAuditLog(getPhrenPath(), "hook_prompt", `status=project_disabled project=${detectedProject}`);
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
const safeQuery = buildRobustFtsQuery(keywords, detectedProject, getPhrenPath());
|
|
166
|
+
if (!safeQuery)
|
|
167
|
+
process.exit(0);
|
|
168
|
+
try {
|
|
169
|
+
const tSearch0 = Date.now();
|
|
170
|
+
let rows = await searchDocumentsAsync(db, safeQuery, prompt, keywords, detectedProject, false, getPhrenPath());
|
|
171
|
+
stage.searchMs = Date.now() - tSearch0;
|
|
172
|
+
if (!rows || !rows.length)
|
|
173
|
+
process.exit(0);
|
|
174
|
+
autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
|
|
175
|
+
const tTrust0 = Date.now();
|
|
176
|
+
const policy = getRetentionPolicy(getPhrenPath());
|
|
177
|
+
const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
|
|
178
|
+
const trustResult = applyTrustFilter(rows, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay, getPhrenPath());
|
|
179
|
+
rows = trustResult.rows;
|
|
180
|
+
stage.trustMs = Date.now() - tTrust0;
|
|
181
|
+
if (!rows.length)
|
|
182
|
+
process.exit(0);
|
|
183
|
+
if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && getProactivityLevelForFindings(getPhrenPath()) !== "low" && sessionId && detectedProject && cwd) {
|
|
184
|
+
const marker = sessionMarker(getPhrenPath(), `extracted-${sessionId}-${detectedProject}`);
|
|
185
|
+
if (!fs.existsSync(marker)) {
|
|
186
|
+
try {
|
|
187
|
+
await handleExtractMemories(detectedProject, cwd, true, sessionId, "hook");
|
|
188
|
+
fs.writeFileSync(marker, "");
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
debugLog(`auto-extract failed for ${detectedProject}: ${errorMessage(err)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const tRank0 = Date.now();
|
|
196
|
+
rows = rankResults(rows, intent, gitCtx, detectedProject, getPhrenPath(), db, cwd, keywords);
|
|
197
|
+
stage.rankMs = Date.now() - tRank0;
|
|
198
|
+
if (!rows.length)
|
|
199
|
+
process.exit(0);
|
|
200
|
+
const safeTokenBudget = clampInt(process.env.PHREN_CONTEXT_TOKEN_BUDGET, 550, 180, 10000);
|
|
201
|
+
const safeLineBudget = clampInt(process.env.PHREN_CONTEXT_SNIPPET_LINES, 6, 2, 100);
|
|
202
|
+
const safeCharBudget = clampInt(process.env.PHREN_CONTEXT_SNIPPET_CHARS, 520, 120, 10000);
|
|
203
|
+
const tSelect0 = Date.now();
|
|
204
|
+
const { selected, usedTokens } = selectSnippets(rows, keywords, safeTokenBudget, safeLineBudget, safeCharBudget);
|
|
205
|
+
stage.selectMs = Date.now() - tSelect0;
|
|
206
|
+
if (!selected.length)
|
|
207
|
+
process.exit(0);
|
|
208
|
+
// Injection budget: cap total injected tokens across all content
|
|
209
|
+
const maxInjectTokens = clampInt(process.env.PHREN_MAX_INJECT_TOKENS, 2000, 200, 20000);
|
|
210
|
+
let budgetSelected = selected;
|
|
211
|
+
let budgetUsedTokens = usedTokens;
|
|
212
|
+
if (budgetUsedTokens > maxInjectTokens) {
|
|
213
|
+
const priorityOrder = (s) => {
|
|
214
|
+
if (s.doc.type === "findings")
|
|
215
|
+
return 0;
|
|
216
|
+
if (s.doc.type === "canonical")
|
|
217
|
+
return 1;
|
|
218
|
+
if (s.doc.type === "summary" || s.doc.type === "claude")
|
|
219
|
+
return 2;
|
|
220
|
+
if (s.doc.type === "reference")
|
|
221
|
+
return 4;
|
|
222
|
+
return 3;
|
|
223
|
+
};
|
|
224
|
+
const sorted = [...budgetSelected].sort((a, b) => priorityOrder(a) - priorityOrder(b));
|
|
225
|
+
const kept = [];
|
|
226
|
+
let runningTokens = 36;
|
|
227
|
+
for (const s of sorted) {
|
|
228
|
+
const est = approximateTokens(s.snippet) + 14;
|
|
229
|
+
if (runningTokens + est <= maxInjectTokens) {
|
|
230
|
+
kept.push(s);
|
|
231
|
+
runningTokens += est;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
budgetSelected = kept;
|
|
235
|
+
budgetUsedTokens = runningTokens;
|
|
236
|
+
debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
|
|
237
|
+
}
|
|
238
|
+
const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getPhrenPath(), sessionId);
|
|
239
|
+
const taskLevel = getProactivityLevelForTask(getPhrenPath());
|
|
240
|
+
const taskLifecycle = handleTaskPromptLifecycle({
|
|
241
|
+
phrenPath: getPhrenPath(),
|
|
242
|
+
prompt,
|
|
243
|
+
project: detectedProject,
|
|
244
|
+
sessionId,
|
|
245
|
+
intent,
|
|
246
|
+
taskLevel,
|
|
247
|
+
});
|
|
248
|
+
if (taskLifecycle.noticeLines.length > 0) {
|
|
249
|
+
parts.push("");
|
|
250
|
+
parts.push(...taskLifecycle.noticeLines);
|
|
251
|
+
}
|
|
252
|
+
// Inject finding sensitivity agent instruction
|
|
253
|
+
try {
|
|
254
|
+
const workflowPolicy = getWorkflowPolicy(getPhrenPath());
|
|
255
|
+
const sensitivity = workflowPolicy.findingSensitivity ?? "balanced";
|
|
256
|
+
const sensitivityConfig = FINDING_SENSITIVITY_CONFIG[sensitivity];
|
|
257
|
+
if (sensitivityConfig) {
|
|
258
|
+
parts.push("");
|
|
259
|
+
parts.push(`[phren finding-sensitivity=${sensitivity}] ${sensitivityConfig.agentInstruction}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// ignore — non-fatal
|
|
264
|
+
}
|
|
265
|
+
// Add budget info to trace
|
|
266
|
+
if (parts.length > 0) {
|
|
267
|
+
const traceIdx = parts.findIndex(p => p.includes("trace:"));
|
|
268
|
+
if (traceIdx !== -1) {
|
|
269
|
+
parts[traceIdx] = parts[traceIdx].replace(/tokens/, `budget=${budgetUsedTokens}/${maxInjectTokens};tokens`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (sessionId) {
|
|
273
|
+
trackSessionMetrics(getPhrenPath(), sessionId, budgetSelected);
|
|
274
|
+
}
|
|
275
|
+
// Reads stay side-effect free: trust filter output informs ranking/snippets now,
|
|
276
|
+
// while queue/audit mutation is deferred to explicit governance maintenance.
|
|
277
|
+
if (trustResult.queueItems.length > 0 || trustResult.auditEntries.length > 0) {
|
|
278
|
+
debugLog(`hook-prompt deferred trust governance items=${trustResult.queueItems.length} audit=${trustResult.auditEntries.length}`);
|
|
279
|
+
}
|
|
280
|
+
const noticeFile = sessionId ? sessionMarker(getPhrenPath(), `noticed-${sessionId}`) : null;
|
|
281
|
+
const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
|
|
282
|
+
if (!alreadyNoticed) {
|
|
283
|
+
// Clean up stale session markers (>24h old) from .sessions/ dir
|
|
284
|
+
try {
|
|
285
|
+
const cutoff = Date.now() - 86400000;
|
|
286
|
+
const sessDir = sessionsDir(getPhrenPath());
|
|
287
|
+
if (fs.existsSync(sessDir)) {
|
|
288
|
+
for (const f of fs.readdirSync(sessDir)) {
|
|
289
|
+
if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
|
|
290
|
+
continue;
|
|
291
|
+
const fp = `${sessDir}/${f}`;
|
|
292
|
+
if (fs.statSync(fp).mtimeMs < cutoff)
|
|
293
|
+
fs.unlinkSync(fp);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Also clean stale markers from the phren root
|
|
297
|
+
for (const f of fs.readdirSync(getPhrenPath())) {
|
|
298
|
+
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
299
|
+
continue;
|
|
300
|
+
const fp = `${getPhrenPath()}/${f}`;
|
|
301
|
+
try {
|
|
302
|
+
fs.unlinkSync(fp);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (process.env.PHREN_DEBUG)
|
|
306
|
+
process.stderr.write(`[phren] hookPrompt staleNoticeUnlink: ${errorMessage(err)}\n`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
|
|
312
|
+
}
|
|
313
|
+
const needed = checkConsolidationNeeded(getPhrenPath(), profile);
|
|
314
|
+
if (needed.length > 0) {
|
|
315
|
+
const notices = needed.map((n) => {
|
|
316
|
+
const since = n.lastConsolidated ? ` since ${n.lastConsolidated}` : "";
|
|
317
|
+
return ` ${n.project}: ${n.entriesSince} new findings${since}`;
|
|
318
|
+
});
|
|
319
|
+
parts.push(`\u25c8 phren \u00b7 consolidation ready`);
|
|
320
|
+
parts.push(`<phren-notice>`);
|
|
321
|
+
parts.push(`Findings ready for consolidation:`);
|
|
322
|
+
parts.push(notices.join("\n"));
|
|
323
|
+
parts.push(`Run phren-consolidate when ready.`);
|
|
324
|
+
parts.push(`<phren-notice>`);
|
|
325
|
+
}
|
|
326
|
+
if (noticeFile) {
|
|
327
|
+
try {
|
|
328
|
+
fs.writeFileSync(noticeFile, "");
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if (process.env.PHREN_DEBUG)
|
|
332
|
+
process.stderr.write(`[phren] hookPrompt noticeFileWrite: ${errorMessage(err)}\n`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const totalMs = stage.indexMs + stage.searchMs + stage.trustMs + stage.rankMs + stage.selectMs;
|
|
337
|
+
const slowThreshold = Number.parseInt(process.env.PHREN_SLOW_FS_WARN_MS || "3000", 10) || 3000;
|
|
338
|
+
if (totalMs > slowThreshold) {
|
|
339
|
+
debugLog(`slow-fs: hook-prompt took ${totalMs}ms (index=${stage.indexMs} search=${stage.searchMs} trust=${stage.trustMs} rank=${stage.rankMs} select=${stage.selectMs})`);
|
|
340
|
+
process.stderr.write(`phren: hook-prompt took ${totalMs}ms, check if ~/.phren is on a slow or network filesystem\n`);
|
|
341
|
+
}
|
|
342
|
+
console.log(parts.join("\n"));
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
const msg = errorMessage(err);
|
|
346
|
+
process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details.<phren-error>\n`);
|
|
347
|
+
debugLog(`hook-prompt error: ${msg}`);
|
|
348
|
+
process.exit(0);
|
|
349
|
+
}
|
|
350
|
+
}
|