@phren/cli 0.0.28 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/dist/capabilities/cli.js +2 -5
- package/mcp/dist/capabilities/mcp.js +5 -8
- package/mcp/dist/capabilities/types.js +2 -5
- package/mcp/dist/capabilities/vscode.js +2 -5
- package/mcp/dist/capabilities/web-ui.js +2 -5
- package/mcp/dist/{cli-actions.js → cli/actions.js} +22 -21
- package/mcp/dist/{cli.js → cli/cli.js} +13 -13
- package/mcp/dist/{cli-config.js → cli/config.js} +9 -9
- package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
- package/mcp/dist/{cli-govern.js → cli/govern.js} +10 -9
- package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
- package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
- package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
- package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
- package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
- package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +42 -57
- package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
- package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
- package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
- package/mcp/dist/{cli-search.js → cli/search.js} +8 -7
- package/mcp/dist/cli-hooks-git.js +243 -0
- package/mcp/dist/cli-hooks-prompt.js +319 -0
- package/mcp/dist/cli-hooks-session-handlers.js +349 -0
- package/mcp/dist/cli-hooks-stop.js +557 -0
- package/mcp/dist/{content-archive.js → content/archive.js} +8 -9
- package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
- package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
- package/mcp/dist/{content-learning.js → content/learning.js} +12 -12
- package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
- package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
- package/mcp/dist/{core-project.js → core/project.js} +4 -4
- package/mcp/dist/{core-search.js → core/search.js} +2 -2
- package/mcp/dist/{data-access.js → data/access.js} +131 -13
- package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
- package/mcp/dist/embedding.js +9 -14
- package/mcp/dist/entrypoint.js +11 -11
- package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
- package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
- package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
- package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +4 -4
- package/mcp/dist/{governance-audit.js → governance/audit.js} +2 -2
- package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
- package/mcp/dist/{governance-policy.js → governance/policy.js} +10 -12
- package/mcp/dist/{governance-rbac.js → governance/rbac.js} +3 -3
- package/mcp/dist/{governance-scores.js → governance/scores.js} +8 -10
- package/mcp/dist/hooks.js +39 -31
- package/mcp/dist/index-query.js +4 -1
- package/mcp/dist/index.js +53 -29
- package/mcp/dist/{init-config.js → init/config.js} +6 -6
- package/mcp/dist/{init.js → init/init.js} +28 -29
- package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
- package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
- package/mcp/dist/{init-shared.js → init/shared.js} +3 -3
- package/mcp/dist/init-bootstrap.js +68 -0
- package/mcp/dist/init-detect.js +38 -0
- package/mcp/dist/init-dryrun.js +55 -0
- package/mcp/dist/init-env.js +114 -0
- package/mcp/dist/init-fresh.js +239 -0
- package/mcp/dist/init-hooks.js +26 -0
- package/mcp/dist/init-mcp.js +65 -0
- package/mcp/dist/init-migrate.js +51 -0
- package/mcp/dist/init-modes.js +135 -0
- package/mcp/dist/init-npm.js +37 -0
- package/mcp/dist/init-project-local.js +99 -0
- package/mcp/dist/init-semantic.js +48 -0
- package/mcp/dist/init-types.js +1 -0
- package/mcp/dist/init-uninstall.js +482 -0
- package/mcp/dist/init-update.js +96 -0
- package/mcp/dist/init-walkthrough-merge.js +90 -0
- package/mcp/dist/init-walkthrough.js +529 -0
- package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
- package/mcp/dist/{link-context.js → link/context.js} +4 -4
- package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
- package/mcp/dist/{link.js → link/link.js} +26 -31
- package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
- package/mcp/dist/logger.js +11 -3
- package/mcp/dist/phren-art.js +0 -6
- package/mcp/dist/phren-paths.js +30 -12
- package/mcp/dist/proactivity.js +2 -2
- package/mcp/dist/profile-store.js +5 -6
- package/mcp/dist/project-config.js +2 -2
- package/mcp/dist/project-topics.js +1 -1
- package/mcp/dist/query-correlation.js +1 -1
- package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
- package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
- package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
- package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +3 -3
- package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
- package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +15 -24
- package/mcp/dist/shared/governance.js +4 -0
- package/mcp/dist/{shared-index.js → shared/index.js} +92 -123
- package/mcp/dist/{shared-ollama.js → shared/ollama.js} +2 -2
- package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +16 -21
- package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +17 -20
- package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
- package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
- package/mcp/dist/shared.js +4 -59
- package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
- package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
- package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
- package/mcp/dist/{shell-render.js → shell/render.js} +1 -1
- package/mcp/dist/{shell.js → shell/shell.js} +11 -11
- package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
- package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
- package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
- package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
- package/mcp/dist/{skill-registry.js → skill/registry.js} +4 -4
- package/mcp/dist/{skill-state.js → skill/state.js} +1 -1
- package/mcp/dist/startup-embedding.js +2 -2
- package/mcp/dist/status.js +15 -14
- package/mcp/dist/{tasks-github.js → task/github.js} +2 -2
- package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
- package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +7 -7
- package/mcp/dist/telemetry.js +3 -4
- package/mcp/dist/tool-registry.js +29 -17
- package/mcp/dist/tools/config.js +515 -0
- package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
- package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
- package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
- package/mcp/dist/{mcp-finding.js → tools/finding.js} +97 -124
- package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
- package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
- package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
- package/mcp/dist/{mcp-ops.js → tools/ops.js} +169 -71
- package/mcp/dist/{mcp-search.js → tools/search.js} +19 -23
- package/mcp/dist/{mcp-session.js → tools/session.js} +48 -23
- package/mcp/dist/{mcp-skills.js → tools/skills.js} +33 -35
- package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
- package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
- package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
- package/mcp/dist/{memory-ui-page.js → ui/page.js} +4 -6
- package/mcp/dist/{memory-ui-server.js → ui/server.js} +30 -22
- package/mcp/dist/update.js +2 -2
- package/mcp/dist/utils.js +51 -11
- package/package.json +2 -2
- package/scripts/preuninstall.mjs +31 -0
- package/starter/global/CLAUDE.md +3 -2
- package/mcp/dist/mcp-config.js +0 -551
- package/mcp/dist/shared-governance.js +0 -4
- /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
- /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
- /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
- /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
- /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
- /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
- /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
- /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop hook handler: git commit/push, background sync, auto-capture, governance.
|
|
3
|
+
* Extracted from cli-hooks-session.ts for modularity.
|
|
4
|
+
*/
|
|
5
|
+
import { buildHookContext, handleGuardSkip, debugLog, runtimeFile, sessionMarker, getPhrenPath, updateRuntimeHealth, appendAuditLog, withFileLock, getWorkflowPolicy, isProjectHookEnabled, ensureLocalGitRepo, getProactivityLevelForTask, getProactivityLevelForFindings, hasExplicitFindingSignal, shouldAutoCaptureFindingsForLevel, FINDING_SENSITIVITY_CONFIG, isFeatureEnabled, errorMessage, bootstrapPhrenDotEnv, finalizeTaskSession, appendFindingJournal, homePath, resolveRuntimeProfile, } from "./cli/hooks-context.js";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import * as os from "os";
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { resolveSubprocessArgs as _resolveSubprocessArgs, runBestEffortGit, countUnsyncedCommits, recoverPushConflict, } from "./cli-hooks-git.js";
|
|
11
|
+
import { logger } from "./logger.js";
|
|
12
|
+
function getRuntimeProfile() {
|
|
13
|
+
return resolveRuntimeProfile(getPhrenPath());
|
|
14
|
+
}
|
|
15
|
+
/** Read JSON from stdin if it's not a TTY. Returns null if stdin is a TTY or parsing fails. */
|
|
16
|
+
export function readStdinJson() {
|
|
17
|
+
if (process.stdin.isTTY)
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(0, "utf-8"));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
logger.debug("readStdinJson", errorMessage(err));
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Validate that a transcript path points to a safe, expected location.
|
|
28
|
+
* Uses realpathSync to dereference symlinks, preventing traversal attacks
|
|
29
|
+
* where a symlink inside a safe dir points outside it.
|
|
30
|
+
*/
|
|
31
|
+
function isSafeTranscriptPath(p) {
|
|
32
|
+
// Resolve symlinks so a link like ~/.claude/evil -> /etc/passwd is caught
|
|
33
|
+
let normalized;
|
|
34
|
+
try {
|
|
35
|
+
normalized = fs.realpathSync.native(p);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// If the file doesn't exist yet, fall back to lexical resolution
|
|
39
|
+
try {
|
|
40
|
+
normalized = fs.realpathSync.native(path.dirname(p));
|
|
41
|
+
normalized = path.join(normalized, path.basename(p));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
normalized = path.resolve(p);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const safePrefixes = [
|
|
48
|
+
path.resolve(os.tmpdir()),
|
|
49
|
+
path.resolve(homePath(".claude")),
|
|
50
|
+
path.resolve(homePath(".config", "claude")),
|
|
51
|
+
];
|
|
52
|
+
return safePrefixes.some(prefix => normalized.startsWith(prefix + path.sep) || normalized === prefix);
|
|
53
|
+
}
|
|
54
|
+
// ── Q21: Conversation memory capture ─────────────────────────────────────────
|
|
55
|
+
const INSIGHT_KEYWORDS = [
|
|
56
|
+
"always", "never", "important", "pitfall", "gotcha", "trick", "workaround",
|
|
57
|
+
"careful", "caveat", "beware", "note that", "make sure",
|
|
58
|
+
"don't forget", "remember to", "must", "avoid", "prefer",
|
|
59
|
+
];
|
|
60
|
+
const INSIGHT_KEYWORD_RE = new RegExp(`\\b(${INSIGHT_KEYWORDS.join("|")})\\b`, "i");
|
|
61
|
+
/**
|
|
62
|
+
* Extract potential insights from conversation text using keyword heuristics.
|
|
63
|
+
* Returns lines that contain insight-signal words and look like actionable knowledge.
|
|
64
|
+
*/
|
|
65
|
+
export function extractConversationInsights(text) {
|
|
66
|
+
const lines = text.split("\n").filter(l => l.trim().length > 20 && l.trim().length < 300);
|
|
67
|
+
const insights = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
// Skip code-only lines, headers, etc.
|
|
72
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("#") || trimmed.startsWith("//"))
|
|
73
|
+
continue;
|
|
74
|
+
if (trimmed.startsWith("$") || trimmed.startsWith(">"))
|
|
75
|
+
continue;
|
|
76
|
+
if (INSIGHT_KEYWORD_RE.test(trimmed) || hasExplicitFindingSignal(trimmed)) {
|
|
77
|
+
// Normalize for dedup
|
|
78
|
+
const normalized = trimmed.toLowerCase().replace(/\s+/g, " ");
|
|
79
|
+
if (!seen.has(normalized)) {
|
|
80
|
+
seen.add(normalized);
|
|
81
|
+
insights.push(trimmed);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Cap to prevent flooding
|
|
86
|
+
return insights.slice(0, 5);
|
|
87
|
+
}
|
|
88
|
+
export function filterConversationInsightsForProactivity(insights, level = getProactivityLevelForFindings(getPhrenPath())) {
|
|
89
|
+
if (level === "high")
|
|
90
|
+
return insights;
|
|
91
|
+
return insights.filter((insight) => shouldAutoCaptureFindingsForLevel(level, insight));
|
|
92
|
+
}
|
|
93
|
+
export function getSessionCap() {
|
|
94
|
+
if (process.env.PHREN_AUTOCAPTURE_SESSION_CAP) {
|
|
95
|
+
return parseInt(process.env.PHREN_AUTOCAPTURE_SESSION_CAP, 10);
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const policy = getWorkflowPolicy(getPhrenPath());
|
|
99
|
+
const sensitivity = policy.findingSensitivity ?? "balanced";
|
|
100
|
+
return FINDING_SENSITIVITY_CONFIG[sensitivity]?.sessionCap ?? 10;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return 10;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function scheduleBackgroundSync(phrenPathLocal) {
|
|
107
|
+
const lockPath = runtimeFile(phrenPathLocal, "background-sync.lock");
|
|
108
|
+
const logPath = runtimeFile(phrenPathLocal, "background-sync.log");
|
|
109
|
+
const spawnArgs = _resolveSubprocessArgs("background-sync");
|
|
110
|
+
if (!spawnArgs)
|
|
111
|
+
return false;
|
|
112
|
+
try {
|
|
113
|
+
if (fs.existsSync(lockPath)) {
|
|
114
|
+
const ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
115
|
+
if (ageMs <= 10 * 60 * 1000)
|
|
116
|
+
return false;
|
|
117
|
+
fs.unlinkSync(lockPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
debugLog(`scheduleBackgroundSync: lock check failed: ${errorMessage(err)}`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
fs.writeFileSync(lockPath, JSON.stringify({ startedAt: new Date().toISOString(), pid: process.pid }) + "\n", { flag: "wx" });
|
|
126
|
+
const logFd = fs.openSync(logPath, "a");
|
|
127
|
+
fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
|
|
128
|
+
const child = spawn(process.execPath, spawnArgs, {
|
|
129
|
+
cwd: process.cwd(),
|
|
130
|
+
detached: true,
|
|
131
|
+
stdio: ["ignore", logFd, logFd],
|
|
132
|
+
env: {
|
|
133
|
+
...process.env,
|
|
134
|
+
PHREN_PATH: phrenPathLocal,
|
|
135
|
+
PHREN_PROFILE: getRuntimeProfile(),
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
child.unref();
|
|
139
|
+
fs.closeSync(logFd);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
try {
|
|
144
|
+
fs.unlinkSync(lockPath);
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
debugLog(`scheduleBackgroundSync: spawn failed: ${errorMessage(err)}`);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function scheduleWeeklyGovernance() {
|
|
152
|
+
try {
|
|
153
|
+
const lastGovPath = runtimeFile(getPhrenPath(), "last-governance.txt");
|
|
154
|
+
const lastRun = fs.existsSync(lastGovPath) ? parseInt(fs.readFileSync(lastGovPath, "utf8"), 10) : 0;
|
|
155
|
+
const daysSince = (Date.now() - lastRun) / 86_400_000;
|
|
156
|
+
if (daysSince >= 7) {
|
|
157
|
+
const spawnArgs = _resolveSubprocessArgs("background-maintenance");
|
|
158
|
+
if (spawnArgs) {
|
|
159
|
+
const child = spawn(process.execPath, spawnArgs, { detached: true, stdio: "ignore" });
|
|
160
|
+
child.unref();
|
|
161
|
+
fs.writeFileSync(lastGovPath, Date.now().toString());
|
|
162
|
+
debugLog("hook_stop: scheduled weekly governance run");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
debugLog(`hook_stop: governance scheduling failed: ${errorMessage(err)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export async function handleHookStop() {
|
|
171
|
+
const ctx = buildHookContext();
|
|
172
|
+
const { phrenPath, activeProject, manifest } = ctx;
|
|
173
|
+
const now = new Date().toISOString();
|
|
174
|
+
bootstrapPhrenDotEnv(phrenPath);
|
|
175
|
+
if (!ctx.hooksEnabled) {
|
|
176
|
+
handleGuardSkip(ctx, "hook_stop", "disabled", {
|
|
177
|
+
lastStopAt: now,
|
|
178
|
+
lastAutoSave: { at: now, status: "clean", detail: "hooks disabled by preference" },
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!ctx.toolHookEnabled) {
|
|
183
|
+
handleGuardSkip(ctx, "hook_stop", `tool_disabled tool=${ctx.hookTool}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (!isProjectHookEnabled(phrenPath, activeProject, "Stop")) {
|
|
187
|
+
handleGuardSkip(ctx, "hook_stop", `project_disabled project=${activeProject}`, {
|
|
188
|
+
lastStopAt: now,
|
|
189
|
+
lastAutoSave: { at: now, status: "clean", detail: `hooks disabled for project ${activeProject}` },
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Read stdin early — it's a stream and can only be consumed once.
|
|
194
|
+
// Needed for auto-capture transcript_path parsing.
|
|
195
|
+
const stdinPayload = readStdinJson();
|
|
196
|
+
const taskSessionId = typeof stdinPayload?.session_id === "string" ? stdinPayload.session_id : undefined;
|
|
197
|
+
const taskLevel = getProactivityLevelForTask(phrenPath);
|
|
198
|
+
if (taskSessionId && taskLevel !== "high") {
|
|
199
|
+
debugLog(`hook-stop task proactivity=${taskLevel}`);
|
|
200
|
+
}
|
|
201
|
+
// Auto-capture BEFORE git operations so captured insights get committed and pushed.
|
|
202
|
+
// Gated behind PHREN_FEATURE_AUTO_CAPTURE=1.
|
|
203
|
+
const findingsLevel = getProactivityLevelForFindings(phrenPath);
|
|
204
|
+
if (isFeatureEnabled("PHREN_FEATURE_AUTO_CAPTURE", false) && findingsLevel !== "low") {
|
|
205
|
+
try {
|
|
206
|
+
let captureInput = process.env.PHREN_CONVERSATION_CONTEXT || "";
|
|
207
|
+
if (!captureInput && stdinPayload?.transcript_path) {
|
|
208
|
+
const transcriptPath = stdinPayload.transcript_path;
|
|
209
|
+
if (!isSafeTranscriptPath(transcriptPath)) {
|
|
210
|
+
debugLog(`auto-capture: skipping unsafe transcript_path: ${transcriptPath}`);
|
|
211
|
+
}
|
|
212
|
+
else if (fs.existsSync(transcriptPath)) {
|
|
213
|
+
// Cap at last 500 lines (~50 KB) to bound memory usage for long sessions
|
|
214
|
+
const raw = fs.readFileSync(transcriptPath, "utf-8");
|
|
215
|
+
const allLines = raw.split("\n").filter(Boolean);
|
|
216
|
+
const lines = allLines.length > 500 ? allLines.slice(-500) : allLines;
|
|
217
|
+
const assistantTexts = [];
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
try {
|
|
220
|
+
const msg = JSON.parse(line);
|
|
221
|
+
if (msg.role !== "assistant")
|
|
222
|
+
continue;
|
|
223
|
+
if (typeof msg.content === "string")
|
|
224
|
+
assistantTexts.push(msg.content);
|
|
225
|
+
else if (Array.isArray(msg.content)) {
|
|
226
|
+
for (const block of msg.content) {
|
|
227
|
+
if (block.type === "text" && block.text)
|
|
228
|
+
assistantTexts.push(block.text);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
logger.debug("hookStop transcriptParse", errorMessage(err));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
captureInput = assistantTexts.join("\n");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (captureInput) {
|
|
240
|
+
if (activeProject) {
|
|
241
|
+
// Check session cap before extracting — same guard as PostToolUse hook
|
|
242
|
+
let capReached = false;
|
|
243
|
+
if (taskSessionId) {
|
|
244
|
+
try {
|
|
245
|
+
const capFile = sessionMarker(phrenPath, `tool-findings-${taskSessionId}`);
|
|
246
|
+
let count = 0;
|
|
247
|
+
if (fs.existsSync(capFile)) {
|
|
248
|
+
count = Number.parseInt(fs.readFileSync(capFile, "utf8").trim(), 10) || 0;
|
|
249
|
+
}
|
|
250
|
+
const cap = getSessionCap();
|
|
251
|
+
if (count >= cap) {
|
|
252
|
+
debugLog(`hook-stop: session cap reached (${count}/${cap}), skipping extraction`);
|
|
253
|
+
capReached = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
logger.debug("hookStop sessionCapCheck", errorMessage(err));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!capReached) {
|
|
261
|
+
const insights = filterConversationInsightsForProactivity(extractConversationInsights(captureInput), findingsLevel);
|
|
262
|
+
for (const insight of insights) {
|
|
263
|
+
appendFindingJournal(phrenPath, activeProject, `[pattern] ${insight}`, {
|
|
264
|
+
source: "hook",
|
|
265
|
+
sessionId: `hook-stop-${Date.now()}`,
|
|
266
|
+
});
|
|
267
|
+
debugLog(`auto-capture: saved insight for ${activeProject}: ${insight.slice(0, 60)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
debugLog(`auto-capture failed: ${errorMessage(err)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
else if (isFeatureEnabled("PHREN_FEATURE_AUTO_CAPTURE", false)) {
|
|
278
|
+
debugLog("auto-capture: skipped because findings proactivity is low");
|
|
279
|
+
}
|
|
280
|
+
// Wrap git operations in a file lock to prevent concurrent agents from fighting
|
|
281
|
+
const gitOpLockPath = path.join(phrenPath, ".runtime", "git-op");
|
|
282
|
+
await withFileLock(gitOpLockPath, async () => {
|
|
283
|
+
if (manifest?.installMode === "project-local") {
|
|
284
|
+
updateRuntimeHealth(phrenPath, {
|
|
285
|
+
lastStopAt: now,
|
|
286
|
+
lastAutoSave: { at: now, status: "saved-local", detail: "project-local mode writes files only" },
|
|
287
|
+
lastSync: {
|
|
288
|
+
lastPushAt: now,
|
|
289
|
+
lastPushStatus: "saved-local",
|
|
290
|
+
lastPushDetail: "project-local mode does not manage git sync",
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
appendAuditLog(phrenPath, "hook_stop", "status=skipped-local");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const gitRepo = ensureLocalGitRepo(phrenPath);
|
|
297
|
+
if (!gitRepo.ok) {
|
|
298
|
+
finalizeTaskSession({
|
|
299
|
+
phrenPath,
|
|
300
|
+
sessionId: taskSessionId,
|
|
301
|
+
status: "error",
|
|
302
|
+
detail: gitRepo.detail,
|
|
303
|
+
});
|
|
304
|
+
updateRuntimeHealth(phrenPath, {
|
|
305
|
+
lastStopAt: now,
|
|
306
|
+
lastAutoSave: { at: now, status: "error", detail: gitRepo.detail },
|
|
307
|
+
lastSync: {
|
|
308
|
+
lastPushAt: now,
|
|
309
|
+
lastPushStatus: "error",
|
|
310
|
+
lastPushDetail: gitRepo.detail,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
appendAuditLog(phrenPath, "hook_stop", `status=error detail=${JSON.stringify(gitRepo.detail)}`);
|
|
314
|
+
process.stderr.write(`phren: git repo error — ${gitRepo.detail}. Run 'phren doctor --fix' for details.\n`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const status = await runBestEffortGit(["status", "--porcelain"], phrenPath);
|
|
318
|
+
if (!status.ok) {
|
|
319
|
+
finalizeTaskSession({
|
|
320
|
+
phrenPath,
|
|
321
|
+
sessionId: taskSessionId,
|
|
322
|
+
status: "error",
|
|
323
|
+
detail: status.error || "git status failed",
|
|
324
|
+
});
|
|
325
|
+
updateRuntimeHealth(phrenPath, {
|
|
326
|
+
lastStopAt: now,
|
|
327
|
+
lastAutoSave: { at: now, status: "error", detail: status.error || "git status failed" },
|
|
328
|
+
lastSync: {
|
|
329
|
+
lastPushAt: now,
|
|
330
|
+
lastPushStatus: "error",
|
|
331
|
+
lastPushDetail: status.error || "git status failed",
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
appendAuditLog(phrenPath, "hook_stop", `status=error detail=${JSON.stringify(status.error || "git status failed")}`);
|
|
335
|
+
process.stderr.write(`phren: git status failed — your changes may not be saved. Run 'phren doctor --fix'.\n`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!status.output) {
|
|
339
|
+
updateRuntimeHealth(phrenPath, {
|
|
340
|
+
lastStopAt: now,
|
|
341
|
+
lastAutoSave: { at: now, status: "clean", detail: "no changes" },
|
|
342
|
+
lastSync: {
|
|
343
|
+
lastPushAt: now,
|
|
344
|
+
lastPushStatus: "saved-pushed",
|
|
345
|
+
lastPushDetail: "no changes",
|
|
346
|
+
unsyncedCommits: 0,
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
appendAuditLog(phrenPath, "hook_stop", "status=clean");
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Stage all changes first, then unstage any sensitive files that slipped
|
|
353
|
+
// through. Using pathspec exclusions with `git add -A` can fail when
|
|
354
|
+
// excluded paths are also gitignored (git treats the pathspec as an error).
|
|
355
|
+
let add = await runBestEffortGit(["add", "-A"], phrenPath);
|
|
356
|
+
if (add.ok) {
|
|
357
|
+
// Belt-and-suspenders: unstage sensitive files that .gitignore should
|
|
358
|
+
// already block. Failures here are non-fatal (files may not exist).
|
|
359
|
+
await runBestEffortGit(["reset", "HEAD", "--", ".env", "**/.env", "*.pem", "*.key"], phrenPath);
|
|
360
|
+
}
|
|
361
|
+
let commitMsg = "auto-save phren";
|
|
362
|
+
if (add.ok) {
|
|
363
|
+
const diff = await runBestEffortGit(["diff", "--cached", "--stat", "--no-color"], phrenPath);
|
|
364
|
+
if (diff.ok && diff.output) {
|
|
365
|
+
// Parse "project/file.md | 3 +++" lines into project names and file types
|
|
366
|
+
const changes = new Map();
|
|
367
|
+
for (const line of diff.output.split("\n")) {
|
|
368
|
+
const m = line.match(/^\s*([^/]+)\/([^|]+)\s*\|/);
|
|
369
|
+
if (!m)
|
|
370
|
+
continue;
|
|
371
|
+
const proj = m[1].trim();
|
|
372
|
+
if (proj.startsWith("."))
|
|
373
|
+
continue; // skip .config, .runtime, etc.
|
|
374
|
+
const file = m[2].trim();
|
|
375
|
+
if (!changes.has(proj))
|
|
376
|
+
changes.set(proj, new Set());
|
|
377
|
+
if (/findings/i.test(file))
|
|
378
|
+
changes.get(proj).add("findings");
|
|
379
|
+
else if (/tasks/i.test(file))
|
|
380
|
+
changes.get(proj).add("task");
|
|
381
|
+
else if (/CLAUDE/i.test(file))
|
|
382
|
+
changes.get(proj).add("config");
|
|
383
|
+
else if (/summary/i.test(file))
|
|
384
|
+
changes.get(proj).add("summary");
|
|
385
|
+
else if (/skill/i.test(file))
|
|
386
|
+
changes.get(proj).add("skills");
|
|
387
|
+
else if (/reference/i.test(file))
|
|
388
|
+
changes.get(proj).add("reference");
|
|
389
|
+
else
|
|
390
|
+
changes.get(proj).add("update");
|
|
391
|
+
}
|
|
392
|
+
if (changes.size > 0) {
|
|
393
|
+
const parts = [...changes.entries()].map(([proj, types]) => `${proj}(${[...types].join(",")})`);
|
|
394
|
+
commitMsg = `phren: ${parts.join(" ")}`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const commit = add.ok ? await runBestEffortGit(["commit", "-m", commitMsg], phrenPath) : { ok: false, error: add.error };
|
|
399
|
+
if (!add.ok || !commit.ok) {
|
|
400
|
+
finalizeTaskSession({
|
|
401
|
+
phrenPath,
|
|
402
|
+
sessionId: taskSessionId,
|
|
403
|
+
status: "error",
|
|
404
|
+
detail: add.error || commit.error || "git add/commit failed",
|
|
405
|
+
});
|
|
406
|
+
updateRuntimeHealth(phrenPath, {
|
|
407
|
+
lastStopAt: now,
|
|
408
|
+
lastAutoSave: {
|
|
409
|
+
at: now,
|
|
410
|
+
status: "error",
|
|
411
|
+
detail: add.error || commit.error || "git add/commit failed",
|
|
412
|
+
},
|
|
413
|
+
lastSync: {
|
|
414
|
+
lastPushAt: now,
|
|
415
|
+
lastPushStatus: "error",
|
|
416
|
+
lastPushDetail: add.error || commit.error || "git add/commit failed",
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
appendAuditLog(phrenPath, "hook_stop", `status=error detail=${JSON.stringify(add.error || commit.error || "git add/commit failed")}`);
|
|
420
|
+
process.stderr.write(`phren: git commit failed — ${add.error || commit.error || "unknown error"}. Changes not saved.\n`);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const remotes = await runBestEffortGit(["remote"], phrenPath);
|
|
424
|
+
if (!remotes.ok || !remotes.output) {
|
|
425
|
+
finalizeTaskSession({
|
|
426
|
+
phrenPath,
|
|
427
|
+
sessionId: taskSessionId,
|
|
428
|
+
status: "saved-local",
|
|
429
|
+
detail: "commit created; no remote configured",
|
|
430
|
+
});
|
|
431
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
432
|
+
updateRuntimeHealth(phrenPath, {
|
|
433
|
+
lastStopAt: now,
|
|
434
|
+
lastAutoSave: { at: now, status: "saved-local", detail: "commit created; no remote configured" },
|
|
435
|
+
lastSync: {
|
|
436
|
+
lastPushAt: now,
|
|
437
|
+
lastPushStatus: "saved-local",
|
|
438
|
+
lastPushDetail: "commit created; no remote configured",
|
|
439
|
+
unsyncedCommits,
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
appendAuditLog(phrenPath, "hook_stop", "status=saved-local");
|
|
443
|
+
if (unsyncedCommits > 3) {
|
|
444
|
+
process.stderr.write(`phren: ${unsyncedCommits} unsynced commits — no git remote configured.\n`);
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
449
|
+
const scheduled = scheduleBackgroundSync(phrenPath);
|
|
450
|
+
const syncDetail = scheduled
|
|
451
|
+
? "commit saved; background sync scheduled"
|
|
452
|
+
: "commit saved; background sync already running";
|
|
453
|
+
finalizeTaskSession({
|
|
454
|
+
phrenPath,
|
|
455
|
+
sessionId: taskSessionId,
|
|
456
|
+
status: "saved-local",
|
|
457
|
+
detail: syncDetail,
|
|
458
|
+
});
|
|
459
|
+
updateRuntimeHealth(phrenPath, {
|
|
460
|
+
lastStopAt: now,
|
|
461
|
+
lastAutoSave: { at: now, status: "saved-local", detail: syncDetail },
|
|
462
|
+
lastSync: {
|
|
463
|
+
lastPushAt: now,
|
|
464
|
+
lastPushStatus: "saved-local",
|
|
465
|
+
lastPushDetail: syncDetail,
|
|
466
|
+
unsyncedCommits,
|
|
467
|
+
},
|
|
468
|
+
});
|
|
469
|
+
appendAuditLog(phrenPath, "hook_stop", `status=saved-local detail=${JSON.stringify(syncDetail)}`);
|
|
470
|
+
}); // end withFileLock(gitOpLockPath)
|
|
471
|
+
// Auto governance scheduling (non-blocking)
|
|
472
|
+
scheduleWeeklyGovernance();
|
|
473
|
+
}
|
|
474
|
+
export async function handleBackgroundSync() {
|
|
475
|
+
const phrenPathLocal = getPhrenPath();
|
|
476
|
+
const now = new Date().toISOString();
|
|
477
|
+
const lockPath = runtimeFile(phrenPathLocal, "background-sync.lock");
|
|
478
|
+
try {
|
|
479
|
+
const remotes = await runBestEffortGit(["remote"], phrenPathLocal);
|
|
480
|
+
if (!remotes.ok || !remotes.output) {
|
|
481
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPathLocal);
|
|
482
|
+
updateRuntimeHealth(phrenPathLocal, {
|
|
483
|
+
lastAutoSave: { at: now, status: "saved-local", detail: "background sync skipped; no remote configured" },
|
|
484
|
+
lastSync: {
|
|
485
|
+
lastPushAt: now,
|
|
486
|
+
lastPushStatus: "saved-local",
|
|
487
|
+
lastPushDetail: "background sync skipped; no remote configured",
|
|
488
|
+
unsyncedCommits,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-local detail=no_remote");
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
const push = await runBestEffortGit(["push"], phrenPathLocal);
|
|
495
|
+
if (push.ok) {
|
|
496
|
+
updateRuntimeHealth(phrenPathLocal, {
|
|
497
|
+
lastAutoSave: { at: now, status: "saved-pushed", detail: "commit pushed by background sync" },
|
|
498
|
+
lastSync: {
|
|
499
|
+
lastPushAt: now,
|
|
500
|
+
lastPushStatus: "saved-pushed",
|
|
501
|
+
lastPushDetail: "commit pushed by background sync",
|
|
502
|
+
unsyncedCommits: 0,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-pushed");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const recovered = await recoverPushConflict(phrenPathLocal);
|
|
509
|
+
if (recovered.ok) {
|
|
510
|
+
updateRuntimeHealth(phrenPathLocal, {
|
|
511
|
+
lastAutoSave: { at: now, status: "saved-pushed", detail: recovered.detail },
|
|
512
|
+
lastSync: {
|
|
513
|
+
lastPullAt: now,
|
|
514
|
+
lastPullStatus: recovered.pullStatus,
|
|
515
|
+
lastPullDetail: recovered.pullDetail,
|
|
516
|
+
lastSuccessfulPullAt: now,
|
|
517
|
+
lastPushAt: now,
|
|
518
|
+
lastPushStatus: "saved-pushed",
|
|
519
|
+
lastPushDetail: recovered.detail,
|
|
520
|
+
unsyncedCommits: 0,
|
|
521
|
+
},
|
|
522
|
+
});
|
|
523
|
+
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-pushed detail=${JSON.stringify(recovered.detail)}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPathLocal);
|
|
527
|
+
const failDetail = recovered.detail || push.error || "background sync push failed";
|
|
528
|
+
updateRuntimeHealth(phrenPathLocal, {
|
|
529
|
+
lastAutoSave: { at: now, status: "saved-local", detail: failDetail },
|
|
530
|
+
lastSync: {
|
|
531
|
+
lastPullAt: now,
|
|
532
|
+
lastPullStatus: recovered.pullStatus,
|
|
533
|
+
lastPullDetail: recovered.pullDetail,
|
|
534
|
+
lastPushAt: now,
|
|
535
|
+
lastPushStatus: "saved-local",
|
|
536
|
+
lastPushDetail: failDetail,
|
|
537
|
+
unsyncedCommits,
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-local detail=${JSON.stringify(failDetail)}`);
|
|
541
|
+
// Append to sync-warnings.jsonl so health_check and session_start can surface recent failures
|
|
542
|
+
try {
|
|
543
|
+
const warningsPath = runtimeFile(phrenPathLocal, "sync-warnings.jsonl");
|
|
544
|
+
const entry = JSON.stringify({ at: now, error: failDetail, unsyncedCommits }) + "\n";
|
|
545
|
+
fs.appendFileSync(warningsPath, entry);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
debugLog(`background-sync: failed to write sync warning: ${errorMessage(err)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
try {
|
|
553
|
+
fs.unlinkSync(lockPath);
|
|
554
|
+
}
|
|
555
|
+
catch { }
|
|
556
|
+
}
|
|
557
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
|
-
import { debugLog, runtimeFile, phrenOk, phrenErr, PhrenError, appendAuditLog, tryUnlink } from "
|
|
5
|
-
import { isValidProjectName, safeProjectPath, errorMessage } from "
|
|
6
|
-
import { withFileLock } from "
|
|
7
|
-
import { appendArchivedEntriesToTopicDoc, classifyTopicForText, readProjectTopics, topicReferencePath } from "
|
|
8
|
-
import { isCitationLine, isArchiveStart, isArchiveEnd, stripComments } from "./
|
|
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 "./metadata.js";
|
|
9
|
+
import { logger } from "../logger.js";
|
|
9
10
|
/**
|
|
10
11
|
* Count active (non-archived) finding entries in FINDINGS.md content.
|
|
11
12
|
* Entries inside archive blocks are considered archived.
|
|
@@ -106,8 +107,7 @@ function buildArchivedBulletSet(referenceDir) {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
catch (err) {
|
|
109
|
-
|
|
110
|
-
process.stderr.write(`[phren] buildArchivedBulletSet: ${errorMessage(err)}\n`);
|
|
110
|
+
logger.debug("archive", `buildArchivedBulletSet: ${errorMessage(err)}`);
|
|
111
111
|
}
|
|
112
112
|
return bulletSet;
|
|
113
113
|
}
|
|
@@ -280,8 +280,7 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
|
|
|
280
280
|
fs.unlinkSync(lockFile);
|
|
281
281
|
}
|
|
282
282
|
catch (err) {
|
|
283
|
-
|
|
284
|
-
process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
|
|
283
|
+
logger.debug("archive", `autoArchiveToReference unlockFile: ${errorMessage(err)}`);
|
|
285
284
|
}
|
|
286
285
|
}
|
|
287
286
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "
|
|
4
|
-
import { errorMessage, runGitOrThrow } from "
|
|
5
|
-
import { findingIdFromLine } from "
|
|
6
|
-
import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./
|
|
7
|
-
import { FINDING_TYPE_DECAY, extractFindingType } from "
|
|
3
|
+
import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "../shared.js";
|
|
4
|
+
import { errorMessage, runGitOrThrow } from "../utils.js";
|
|
5
|
+
import { findingIdFromLine } from "../finding/impact.js";
|
|
6
|
+
import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./metadata.js";
|
|
7
|
+
import { FINDING_TYPE_DECAY, extractFindingType } from "../finding/lifecycle.js";
|
|
8
8
|
export const FINDING_PROVENANCE_SOURCES = [
|
|
9
9
|
"human",
|
|
10
10
|
"agent",
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
|
-
import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "
|
|
5
|
-
import { isFeatureEnabled, safeProjectPath, errorMessage } from "
|
|
6
|
-
import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "
|
|
7
|
-
import { isInactiveFindingLine } from "
|
|
4
|
+
import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "../shared.js";
|
|
5
|
+
import { isFeatureEnabled, safeProjectPath, errorMessage } from "../utils.js";
|
|
6
|
+
import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "../phren-core.js";
|
|
7
|
+
import { isInactiveFindingLine } from "../finding/lifecycle.js";
|
|
8
|
+
import { logger } from "../logger.js";
|
|
8
9
|
// ── LLM provider abstraction ────────────────────────────────────────────────
|
|
9
10
|
const MAX_CACHE_ENTRIES = 500;
|
|
10
11
|
function loadCache(cachePath) {
|
|
@@ -50,8 +51,7 @@ async function withCache(cachePath, key, ttlMs, compute) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
catch (err) {
|
|
53
|
-
|
|
54
|
-
process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
|
|
54
|
+
logger.debug("dedup", `withCache load (${path.basename(cachePath)}): ${errorMessage(err)}`);
|
|
55
55
|
}
|
|
56
56
|
const result = await compute();
|
|
57
57
|
// Persist result
|
|
@@ -61,8 +61,7 @@ async function withCache(cachePath, key, ttlMs, compute) {
|
|
|
61
61
|
persistCache(cachePath, cache);
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
64
|
-
|
|
65
|
-
process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
|
|
64
|
+
logger.debug("dedup", `withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}`);
|
|
66
65
|
}
|
|
67
66
|
return result;
|
|
68
67
|
}
|
|
@@ -562,8 +561,7 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
|
|
|
562
561
|
return { name: e.name, mtime: fs.statSync(fp).mtimeMs, fp };
|
|
563
562
|
}
|
|
564
563
|
catch (err) {
|
|
565
|
-
|
|
566
|
-
process.stderr.write(`[phren] crossProjectScan stat: ${errorMessage(err)}\n`);
|
|
564
|
+
logger.debug("dedup", `crossProjectScan stat: ${errorMessage(err)}`);
|
|
567
565
|
return null;
|
|
568
566
|
}
|
|
569
567
|
})
|
|
@@ -577,8 +575,7 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
|
|
|
577
575
|
}
|
|
578
576
|
}
|
|
579
577
|
catch (err) {
|
|
580
|
-
|
|
581
|
-
process.stderr.write(`[phren] crossProjectScan: ${errorMessage(err)}\n`);
|
|
578
|
+
logger.debug("dedup", `crossProjectScan: ${errorMessage(err)}`);
|
|
582
579
|
}
|
|
583
580
|
const annotations = [];
|
|
584
581
|
const deadline = Date.now() + CONFLICT_CHECK_TOTAL_TIMEOUT_MS;
|