@phren/cli 0.0.9 → 0.0.11
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/README.md +2 -8
- package/mcp/dist/cli-actions.js +5 -5
- package/mcp/dist/cli-config.js +334 -127
- package/mcp/dist/cli-govern.js +140 -3
- package/mcp/dist/cli-graph.js +3 -2
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +3 -3
- package/mcp/dist/cli-hooks.js +41 -34
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/content-archive.js +2 -2
- package/mcp/dist/content-citation.js +12 -22
- package/mcp/dist/content-dedup.js +9 -9
- package/mcp/dist/data-access.js +1 -1
- package/mcp/dist/data-tasks.js +23 -0
- package/mcp/dist/embedding.js +7 -7
- package/mcp/dist/entrypoint.js +129 -102
- package/mcp/dist/governance-locks.js +6 -5
- package/mcp/dist/governance-policy.js +155 -2
- package/mcp/dist/governance-scores.js +3 -3
- package/mcp/dist/hooks.js +39 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +3 -24
- package/mcp/dist/init-setup.js +5 -5
- package/mcp/dist/init.js +170 -23
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +1 -1
- package/mcp/dist/link-doctor.js +3 -3
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +17 -27
- package/mcp/dist/machine-identity.js +1 -9
- package/mcp/dist/mcp-config.js +247 -42
- package/mcp/dist/mcp-data.js +9 -9
- package/mcp/dist/mcp-extract-facts.js +1 -1
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +6 -6
- package/mcp/dist/mcp-graph.js +11 -11
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +8 -8
- package/mcp/dist/mcp-tasks.js +21 -1
- package/mcp/dist/memory-ui-page.js +23 -0
- package/mcp/dist/memory-ui-scripts.js +210 -27
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-paths.js +7 -7
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +63 -16
- package/mcp/dist/session-utils.js +3 -2
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +22 -56
- package/mcp/dist/shared-search-fallback.js +13 -13
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +3 -3
- package/mcp/dist/shell-input.js +1 -1
- package/mcp/dist/shell-state-store.js +1 -1
- package/mcp/dist/shell-view.js +3 -2
- package/mcp/dist/shell.js +1 -1
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +41 -13
- package/mcp/dist/task-hygiene.js +1 -1
- package/mcp/dist/telemetry.js +5 -4
- package/mcp/dist/update.js +1 -1
- package/mcp/dist/utils.js +3 -3
- package/package.json +2 -2
- package/starter/global/skills/audit.md +106 -0
- package/mcp/dist/shared-paths.js +0 -1
package/mcp/dist/cli-govern.js
CHANGED
|
@@ -144,7 +144,7 @@ export async function handlePruneMemories(args = []) {
|
|
|
144
144
|
.filter((e) => e !== null);
|
|
145
145
|
}
|
|
146
146
|
catch (err) {
|
|
147
|
-
if ((process.env.PHREN_DEBUG
|
|
147
|
+
if ((process.env.PHREN_DEBUG))
|
|
148
148
|
process.stderr.write(`[phren] cli-govern retrievalLog readParse: ${errorMessage(err)}\n`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
@@ -227,6 +227,139 @@ export async function handleConsolidateMemories(args = []) {
|
|
|
227
227
|
return;
|
|
228
228
|
console.log(`Updated backups (${backups.length}): ${backups.join(", ")}`);
|
|
229
229
|
}
|
|
230
|
+
export async function handleGcMaintain(args = []) {
|
|
231
|
+
const dryRun = args.includes("--dry-run");
|
|
232
|
+
const phrenPath = getPhrenPath();
|
|
233
|
+
const { execSync } = await import("child_process");
|
|
234
|
+
const report = {
|
|
235
|
+
gitGcRan: false,
|
|
236
|
+
commitsSquashed: 0,
|
|
237
|
+
sessionsRemoved: 0,
|
|
238
|
+
runtimeLogsRemoved: 0,
|
|
239
|
+
};
|
|
240
|
+
// 1. Run git gc --aggressive on the ~/.phren repo
|
|
241
|
+
if (dryRun) {
|
|
242
|
+
console.log("[dry-run] Would run: git gc --aggressive");
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
try {
|
|
246
|
+
execSync("git gc --aggressive --quiet", { cwd: phrenPath, stdio: "pipe" });
|
|
247
|
+
report.gitGcRan = true;
|
|
248
|
+
console.log("git gc --aggressive: done");
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`git gc failed: ${errorMessage(err)}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 2. Squash old auto-save commits (older than 7 days) into weekly summaries
|
|
255
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
|
|
256
|
+
let oldCommits = [];
|
|
257
|
+
try {
|
|
258
|
+
const raw = execSync(`git log --before="${sevenDaysAgo}" --format="%H %ci %s"`, { cwd: phrenPath, encoding: "utf8" }).trim();
|
|
259
|
+
if (raw) {
|
|
260
|
+
oldCommits = raw.split("\n").filter((l) => l.includes("auto-save:") || l.includes("[auto]"));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Not a git repo or no commits — skip silently
|
|
265
|
+
}
|
|
266
|
+
if (oldCommits.length === 0) {
|
|
267
|
+
console.log("Commit squash: no old auto-save commits to squash.");
|
|
268
|
+
}
|
|
269
|
+
else if (dryRun) {
|
|
270
|
+
console.log(`[dry-run] Would squash ${oldCommits.length} auto-save commits older than 7 days into weekly summaries.`);
|
|
271
|
+
report.commitsSquashed = oldCommits.length;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Group by ISO week based on commit timestamp (already in the log output)
|
|
275
|
+
const commitsByWeek = new Map();
|
|
276
|
+
for (const line of oldCommits) {
|
|
277
|
+
const hash = line.split(" ")[0];
|
|
278
|
+
// Format: "HASH YYYY-MM-DD HH:MM:SS +ZZZZ subject..."
|
|
279
|
+
const dateMatch = line.match(/^[a-f0-9]+ (\d{4}-\d{2}-\d{2})/);
|
|
280
|
+
if (!dateMatch)
|
|
281
|
+
continue;
|
|
282
|
+
const date = new Date(dateMatch[1]);
|
|
283
|
+
const weekStart = new Date(date);
|
|
284
|
+
weekStart.setDate(date.getDate() - date.getDay());
|
|
285
|
+
const weekKey = weekStart.toISOString().slice(0, 10);
|
|
286
|
+
if (!commitsByWeek.has(weekKey))
|
|
287
|
+
commitsByWeek.set(weekKey, []);
|
|
288
|
+
commitsByWeek.get(weekKey).push(hash);
|
|
289
|
+
}
|
|
290
|
+
// For each week with multiple commits, soft-reset to oldest and amend into a summary
|
|
291
|
+
for (const [weekKey, hashes] of commitsByWeek.entries()) {
|
|
292
|
+
if (hashes.length < 2)
|
|
293
|
+
continue;
|
|
294
|
+
try {
|
|
295
|
+
const oldest = hashes[hashes.length - 1];
|
|
296
|
+
const newest = hashes[0];
|
|
297
|
+
// Use git rebase --onto to squash: squash all into the oldest parent
|
|
298
|
+
const parentOfOldest = execSync(`git rev-parse ${oldest}^`, { cwd: phrenPath, encoding: "utf8" }).trim();
|
|
299
|
+
// Build rebase script via env variable to squash all but first to "squash"
|
|
300
|
+
const rebaseScript = hashes
|
|
301
|
+
.map((h, i) => `${i === hashes.length - 1 ? "pick" : "squash"} ${h} auto-save`)
|
|
302
|
+
.reverse()
|
|
303
|
+
.join("\n");
|
|
304
|
+
const scriptPath = path.join(phrenPath, ".runtime", `gc-rebase-${weekKey}.sh`);
|
|
305
|
+
fs.writeFileSync(scriptPath, rebaseScript);
|
|
306
|
+
// Use GIT_SEQUENCE_EDITOR to feed our script
|
|
307
|
+
execSync(`GIT_SEQUENCE_EDITOR="cat ${scriptPath} >" git rebase -i ${parentOfOldest}`, { cwd: phrenPath, stdio: "pipe" });
|
|
308
|
+
fs.unlinkSync(scriptPath);
|
|
309
|
+
report.commitsSquashed += hashes.length - 1;
|
|
310
|
+
console.log(`Squashed ${hashes.length} auto-save commits for week of ${weekKey} (${newest.slice(0, 7)}..${oldest.slice(0, 7)}).`);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Squashing is best-effort — log and continue
|
|
314
|
+
console.warn(` Could not squash auto-save commits for week ${weekKey} (possibly non-linear history). Skipping.`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (report.commitsSquashed === 0) {
|
|
318
|
+
console.log("Commit squash: all old auto-save weeks have only one commit, nothing to squash.");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
// 3–4. Prune stale files from .sessions/ and .runtime/
|
|
322
|
+
const thirtyDaysAgo = Date.now() - 30 * 86400000;
|
|
323
|
+
function pruneStaleFiles(dir, label, filter) {
|
|
324
|
+
let removed = 0;
|
|
325
|
+
if (!fs.existsSync(dir))
|
|
326
|
+
return removed;
|
|
327
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
328
|
+
if (filter && !filter(entry))
|
|
329
|
+
continue;
|
|
330
|
+
const fullPath = path.join(dir, entry);
|
|
331
|
+
try {
|
|
332
|
+
if (fs.statSync(fullPath).mtimeMs < thirtyDaysAgo) {
|
|
333
|
+
if (dryRun) {
|
|
334
|
+
console.log(`[dry-run] Would remove: ${label}/${entry}`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
fs.unlinkSync(fullPath);
|
|
338
|
+
}
|
|
339
|
+
removed++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch { /* skip unreadable */ }
|
|
343
|
+
}
|
|
344
|
+
return removed;
|
|
345
|
+
}
|
|
346
|
+
const logExtensions = new Set([".log", ".jsonl", ".json"]);
|
|
347
|
+
const protectedFiles = new Set(["audit.log", "telemetry.json"]);
|
|
348
|
+
report.sessionsRemoved = pruneStaleFiles(path.join(phrenPath, ".sessions"), ".sessions");
|
|
349
|
+
report.runtimeLogsRemoved = pruneStaleFiles(path.join(phrenPath, ".runtime"), ".runtime", (entry) => logExtensions.has(path.extname(entry)) && !protectedFiles.has(entry));
|
|
350
|
+
const verb = dryRun ? "Would remove" : "Removed";
|
|
351
|
+
console.log(`${verb} ${report.sessionsRemoved} stale session marker(s) from .sessions/`);
|
|
352
|
+
console.log(`${verb} ${report.runtimeLogsRemoved} stale runtime log(s) from .runtime/`);
|
|
353
|
+
// 5. Summary
|
|
354
|
+
if (!dryRun) {
|
|
355
|
+
appendAuditLog(phrenPath, "maintain_gc", `gitGc=${report.gitGcRan} squashed=${report.commitsSquashed} sessions=${report.sessionsRemoved} logs=${report.runtimeLogsRemoved}`);
|
|
356
|
+
}
|
|
357
|
+
console.log(`\nGC complete:${dryRun ? " (dry-run)" : ""}` +
|
|
358
|
+
` git_gc=${report.gitGcRan}` +
|
|
359
|
+
` commits_squashed=${report.commitsSquashed}` +
|
|
360
|
+
` sessions_pruned=${report.sessionsRemoved}` +
|
|
361
|
+
` logs_pruned=${report.runtimeLogsRemoved}`);
|
|
362
|
+
}
|
|
230
363
|
// ── Maintain router ──────────────────────────────────────────────────────────
|
|
231
364
|
export async function handleMaintain(args) {
|
|
232
365
|
const sub = args[0];
|
|
@@ -245,6 +378,8 @@ export async function handleMaintain(args) {
|
|
|
245
378
|
return handleExtractMemories(rest[0]);
|
|
246
379
|
case "restore":
|
|
247
380
|
return handleRestoreBackup(rest);
|
|
381
|
+
case "gc":
|
|
382
|
+
return handleGcMaintain(rest);
|
|
248
383
|
default:
|
|
249
384
|
console.log(`phren maintain - memory maintenance and governance
|
|
250
385
|
|
|
@@ -258,7 +393,9 @@ Subcommands:
|
|
|
258
393
|
Deduplicate FINDINGS.md bullets. Run after a burst of work
|
|
259
394
|
when findings feel repetitive, or monthly to keep things clean.
|
|
260
395
|
phren maintain extract [project] Mine git/GitHub signals into memory candidates
|
|
261
|
-
phren maintain restore [project] List and restore from .bak files
|
|
396
|
+
phren maintain restore [project] List and restore from .bak files
|
|
397
|
+
phren maintain gc [--dry-run] Garbage-collect the ~/.phren repo: git gc, squash old
|
|
398
|
+
auto-save commits, prune stale session markers and runtime logs`);
|
|
262
399
|
if (sub) {
|
|
263
400
|
console.error(`\nUnknown maintain subcommand: "${sub}"`);
|
|
264
401
|
process.exit(1);
|
|
@@ -364,7 +501,7 @@ export async function handleBackgroundMaintenance(projectArg) {
|
|
|
364
501
|
fs.unlinkSync(markers.lock);
|
|
365
502
|
}
|
|
366
503
|
catch (err) {
|
|
367
|
-
if ((process.env.PHREN_DEBUG
|
|
504
|
+
if ((process.env.PHREN_DEBUG))
|
|
368
505
|
process.stderr.write(`[phren] cli-govern backgroundMaintenance unlockFinal: ${errorMessage(err)}\n`);
|
|
369
506
|
}
|
|
370
507
|
}
|
package/mcp/dist/cli-graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPhrenPath } from "./shared.js";
|
|
2
2
|
import { buildIndex, queryRows } from "./shared-index.js";
|
|
3
3
|
import { resolveRuntimeProfile } from "./runtime-profile.js";
|
|
4
|
+
import { errorMessage } from "./utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* CLI: phren graph [--project <name>] [--limit <n>]
|
|
6
7
|
* Displays the fragment knowledge graph as a table.
|
|
@@ -115,7 +116,7 @@ export async function handleGraphLink(args) {
|
|
|
115
116
|
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, "mentions", sourceDoc]);
|
|
116
117
|
}
|
|
117
118
|
catch (err) {
|
|
118
|
-
console.error(`Failed to link: ${
|
|
119
|
+
console.error(`Failed to link: ${errorMessage(err)}`);
|
|
119
120
|
process.exit(1);
|
|
120
121
|
}
|
|
121
122
|
// Persist to manual-links.json
|
|
@@ -144,7 +145,7 @@ export async function handleGraphLink(args) {
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
catch (err) {
|
|
147
|
-
console.error(`Failed to persist manual link: ${
|
|
148
|
+
console.error(`Failed to persist manual link: ${errorMessage(err)}`);
|
|
148
149
|
process.exit(1);
|
|
149
150
|
}
|
|
150
151
|
console.log(`Linked "${fragmentName}" to ${sourceDoc}.`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { capCache } from "./shared.js";
|
|
4
|
+
import { errorMessage } from "./utils.js";
|
|
4
5
|
// ── Glob matching and project frontmatter ────────────────────────────────────
|
|
5
6
|
const projectGlobCache = new Map();
|
|
6
7
|
export function clearProjectGlobCache() {
|
|
@@ -45,7 +46,7 @@ function parseProjectGlobs(phrenPathLocal, project) {
|
|
|
45
46
|
}
|
|
46
47
|
catch (err) {
|
|
47
48
|
if (process.env.PHREN_DEBUG)
|
|
48
|
-
process.stderr.write(`[phren] getProjectGlobs: ${
|
|
49
|
+
process.stderr.write(`[phren] getProjectGlobs: ${errorMessage(err)}\n`);
|
|
49
50
|
}
|
|
50
51
|
projectGlobCache.set(project, globs);
|
|
51
52
|
capCache(projectGlobCache);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { recordInjection, recordRetrieval, } from "./shared-governance.js";
|
|
2
2
|
import { getDocSourceKey, } from "./shared-index.js";
|
|
3
3
|
import { logImpact, extractFindingIdsFromSnippet, } from "./finding-impact.js";
|
|
4
|
-
import { isFeatureEnabled } from "./utils.js";
|
|
4
|
+
import { isFeatureEnabled, errorMessage } from "./utils.js";
|
|
5
5
|
import { annotateStale } from "./cli-hooks-citations.js";
|
|
6
6
|
import { approximateTokens, fileRelevanceBoost, branchMatchBoost } from "./shared-retrieval.js";
|
|
7
7
|
// ── Progressive disclosure helpers ────────────────────────────────────────────
|
|
@@ -56,7 +56,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
56
56
|
}
|
|
57
57
|
catch (err) {
|
|
58
58
|
if (process.env.PHREN_DEBUG)
|
|
59
|
-
process.stderr.write(`[phren] injectContext recordRetrieval: ${
|
|
59
|
+
process.stderr.write(`[phren] injectContext recordRetrieval: ${errorMessage(err)}\n`);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
}
|
|
@@ -108,7 +108,7 @@ export function buildHookOutput(selected, usedTokens, intent, gitCtx, detectedPr
|
|
|
108
108
|
}
|
|
109
109
|
catch (err) {
|
|
110
110
|
if (process.env.PHREN_DEBUG)
|
|
111
|
-
process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${
|
|
111
|
+
process.stderr.write(`[phren] injectContext recordRetrievalOrdered: ${errorMessage(err)}\n`);
|
|
112
112
|
}
|
|
113
113
|
parts.push(`[${getDocSourceKey(doc, phrenPathLocal)}] (${doc.type})`);
|
|
114
114
|
parts.push(annotateStale(snippet));
|
package/mcp/dist/cli-hooks.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// cli-hooks-output.ts — hook output formatting
|
|
6
6
|
// cli-hooks-globs.ts — project glob matching
|
|
7
7
|
import { debugLog, sessionMarker, sessionsDir, getPhrenPath, } from "./shared.js";
|
|
8
|
-
import {
|
|
8
|
+
import { mergeConfig, } from "./shared-governance.js";
|
|
9
9
|
import { buildIndex, detectProject, } from "./shared-index.js";
|
|
10
10
|
import { isProjectHookEnabled } from "./project-config.js";
|
|
11
11
|
import { checkConsolidationNeeded, } from "./shared-content.js";
|
|
@@ -126,7 +126,7 @@ export async function handleHookPrompt() {
|
|
|
126
126
|
}
|
|
127
127
|
catch (err) {
|
|
128
128
|
if (process.env.PHREN_DEBUG)
|
|
129
|
-
process.stderr.write(`[phren] hookPrompt stdinRead: ${
|
|
129
|
+
process.stderr.write(`[phren] hookPrompt stdinRead: ${errorMessage(err)}\n`);
|
|
130
130
|
process.exit(0);
|
|
131
131
|
}
|
|
132
132
|
const input = parseHookInput(raw);
|
|
@@ -182,6 +182,7 @@ export async function handleHookPrompt() {
|
|
|
182
182
|
appendAuditLog(getPhrenPath(), "hook_prompt", `status=project_disabled project=${detectedProject}`);
|
|
183
183
|
process.exit(0);
|
|
184
184
|
}
|
|
185
|
+
const resolvedConfig = mergeConfig(getPhrenPath(), detectedProject ?? undefined);
|
|
185
186
|
const safeQuery = buildRobustFtsQuery(keywords, detectedProject, getPhrenPath());
|
|
186
187
|
if (!safeQuery)
|
|
187
188
|
process.exit(0);
|
|
@@ -193,14 +194,17 @@ export async function handleHookPrompt() {
|
|
|
193
194
|
process.exit(0);
|
|
194
195
|
autoLearnQuerySynonyms(getPhrenPath(), detectedProject, keywordEntries, rows);
|
|
195
196
|
const tTrust0 = Date.now();
|
|
196
|
-
const policy =
|
|
197
|
+
const policy = resolvedConfig.retentionPolicy;
|
|
197
198
|
const memoryTtlDays = Number.parseInt(process.env.PHREN_MEMORY_TTL_DAYS || String(policy.ttlDays), 10);
|
|
198
199
|
const trustResult = applyTrustFilter(rows, Number.isNaN(memoryTtlDays) ? policy.ttlDays : memoryTtlDays, policy.minInjectConfidence, policy.decay, getPhrenPath());
|
|
199
200
|
rows = trustResult.rows;
|
|
200
201
|
stage.trustMs = Date.now() - tTrust0;
|
|
201
202
|
if (!rows.length)
|
|
202
203
|
process.exit(0);
|
|
203
|
-
|
|
204
|
+
const findingsProactivity = resolvedConfig.proactivity.findings
|
|
205
|
+
?? resolvedConfig.proactivity.base
|
|
206
|
+
?? getProactivityLevelForFindings(getPhrenPath());
|
|
207
|
+
if (isFeatureEnabled("PHREN_FEATURE_AUTO_EXTRACT", true) && findingsProactivity !== "low" && sessionId && detectedProject && cwd) {
|
|
204
208
|
const marker = sessionMarker(getPhrenPath(), `extracted-${sessionId}-${detectedProject}`);
|
|
205
209
|
if (!fs.existsSync(marker)) {
|
|
206
210
|
try {
|
|
@@ -260,7 +264,9 @@ export async function handleHookPrompt() {
|
|
|
260
264
|
debugLog(`injection-budget: trimmed ${selected.length} -> ${kept.length} snippets to fit ${maxInjectTokens} token budget`);
|
|
261
265
|
}
|
|
262
266
|
const parts = buildHookOutput(budgetSelected, budgetUsedTokens, intent, gitCtx, detectedProject, stage, safeTokenBudget, getPhrenPath(), sessionId);
|
|
263
|
-
const taskLevel =
|
|
267
|
+
const taskLevel = resolvedConfig.proactivity.tasks
|
|
268
|
+
?? resolvedConfig.proactivity.base
|
|
269
|
+
?? getProactivityLevelForTask(getPhrenPath());
|
|
264
270
|
const taskLifecycle = handleTaskPromptLifecycle({
|
|
265
271
|
phrenPath: getPhrenPath(),
|
|
266
272
|
prompt,
|
|
@@ -275,8 +281,7 @@ export async function handleHookPrompt() {
|
|
|
275
281
|
}
|
|
276
282
|
// Inject finding sensitivity agent instruction
|
|
277
283
|
try {
|
|
278
|
-
const
|
|
279
|
-
const sensitivity = workflowPolicy.findingSensitivity ?? "balanced";
|
|
284
|
+
const sensitivity = resolvedConfig.findingSensitivity ?? "balanced";
|
|
280
285
|
const sensitivityConfig = FINDING_SENSITIVITY_CONFIG[sensitivity];
|
|
281
286
|
if (sensitivityConfig) {
|
|
282
287
|
parts.push("");
|
|
@@ -304,36 +309,38 @@ export async function handleHookPrompt() {
|
|
|
304
309
|
const noticeFile = sessionId ? sessionMarker(getPhrenPath(), `noticed-${sessionId}`) : null;
|
|
305
310
|
const alreadyNoticed = noticeFile ? fs.existsSync(noticeFile) : false;
|
|
306
311
|
if (!alreadyNoticed) {
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
312
|
+
// Defer stale session marker cleanup to after output — it's I/O-heavy and not needed for results
|
|
313
|
+
setImmediate(() => {
|
|
314
|
+
try {
|
|
315
|
+
const cutoff = Date.now() - 86400000;
|
|
316
|
+
const sessDir = sessionsDir(getPhrenPath());
|
|
317
|
+
if (fs.existsSync(sessDir)) {
|
|
318
|
+
for (const f of fs.readdirSync(sessDir)) {
|
|
319
|
+
if (!f.startsWith("noticed-") && !f.startsWith("extracted-"))
|
|
320
|
+
continue;
|
|
321
|
+
const fp = `${sessDir}/${f}`;
|
|
322
|
+
try {
|
|
323
|
+
if (fs.statSync(fp).mtimeMs < cutoff)
|
|
324
|
+
fs.unlinkSync(fp);
|
|
325
|
+
}
|
|
326
|
+
catch { /* ignore per-file errors */ }
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Also clean stale markers from the phren root
|
|
330
|
+
for (const f of fs.readdirSync(getPhrenPath())) {
|
|
331
|
+
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
314
332
|
continue;
|
|
315
|
-
const fp = `${
|
|
316
|
-
|
|
333
|
+
const fp = `${getPhrenPath()}/${f}`;
|
|
334
|
+
try {
|
|
317
335
|
fs.unlinkSync(fp);
|
|
336
|
+
}
|
|
337
|
+
catch { /* ignore */ }
|
|
318
338
|
}
|
|
319
339
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!f.startsWith(".noticed-") && !f.startsWith(".extracted-"))
|
|
323
|
-
continue;
|
|
324
|
-
const fp = `${getPhrenPath()}/${f}`;
|
|
325
|
-
try {
|
|
326
|
-
fs.unlinkSync(fp);
|
|
327
|
-
}
|
|
328
|
-
catch (err) {
|
|
329
|
-
if (process.env.PHREN_DEBUG)
|
|
330
|
-
process.stderr.write(`[phren] hookPrompt staleNoticeUnlink: ${errorMessage(err)}\n`);
|
|
331
|
-
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
|
|
332
342
|
}
|
|
333
|
-
}
|
|
334
|
-
catch (err) {
|
|
335
|
-
debugLog(`stale notice cleanup failed: ${errorMessage(err)}`);
|
|
336
|
-
}
|
|
343
|
+
});
|
|
337
344
|
const needed = checkConsolidationNeeded(getPhrenPath(), profile);
|
|
338
345
|
if (needed.length > 0) {
|
|
339
346
|
const notices = needed.map((n) => {
|
|
@@ -345,7 +352,7 @@ export async function handleHookPrompt() {
|
|
|
345
352
|
parts.push(`Findings ready for consolidation:`);
|
|
346
353
|
parts.push(notices.join("\n"));
|
|
347
354
|
parts.push(`Run phren-consolidate when ready.`);
|
|
348
|
-
parts.push(
|
|
355
|
+
parts.push(`<phren-notice>`);
|
|
349
356
|
}
|
|
350
357
|
if (noticeFile) {
|
|
351
358
|
try {
|
|
@@ -367,7 +374,7 @@ export async function handleHookPrompt() {
|
|
|
367
374
|
}
|
|
368
375
|
catch (err) {
|
|
369
376
|
const msg = errorMessage(err);
|
|
370
|
-
process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details
|
|
377
|
+
process.stdout.write(`\n<phren-error>phren hook failed: ${msg}. Check ~/.phren/.runtime/debug.log for details.<phren-error>\n`);
|
|
371
378
|
debugLog(`hook-prompt error: ${msg}`);
|
|
372
379
|
process.exit(0);
|
|
373
380
|
}
|
|
@@ -5,6 +5,7 @@ import { expandHomePath, findProjectNameCaseInsensitive, getPhrenPath, getProjec
|
|
|
5
5
|
import { isValidProjectName, errorMessage } from "./utils.js";
|
|
6
6
|
import { readInstallPreferences, writeInstallPreferences } from "./init-preferences.js";
|
|
7
7
|
import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
|
|
8
|
+
import { detectSkillCollisions } from "./link-skills.js";
|
|
8
9
|
import { setSkillEnabledAndSync, syncSkillLinksForScope } from "./skill-files.js";
|
|
9
10
|
import { findProjectDir } from "./project-locator.js";
|
|
10
11
|
import { TASK_FILE_ALIASES, addTask, completeTask, updateTask, reorderTask, pinTask, removeTask, workNextTask, tidyDoneTasks, linkTaskIssue, promoteTask, resolveTaskItem } from "./data-tasks.js";
|
|
@@ -76,7 +77,7 @@ function openInEditor(filePath) {
|
|
|
76
77
|
execFileSync(editor, [filePath], { stdio: "inherit" });
|
|
77
78
|
}
|
|
78
79
|
catch (err) {
|
|
79
|
-
if ((process.env.PHREN_DEBUG
|
|
80
|
+
if ((process.env.PHREN_DEBUG))
|
|
80
81
|
process.stderr.write(`[phren] openInEditor: ${errorMessage(err)}\n`);
|
|
81
82
|
console.error(`Editor "${editor}" failed. Set $EDITOR to your preferred editor.`);
|
|
82
83
|
process.exit(1);
|
|
@@ -145,7 +146,7 @@ export function handleSkillsNamespace(args, profile) {
|
|
|
145
146
|
console.log(`Linked skill ${fileName} into ${project}.`);
|
|
146
147
|
}
|
|
147
148
|
catch (err) {
|
|
148
|
-
if ((process.env.PHREN_DEBUG
|
|
149
|
+
if ((process.env.PHREN_DEBUG))
|
|
149
150
|
process.stderr.write(`[phren] skill add symlinkFailed: ${errorMessage(err)}\n`);
|
|
150
151
|
fs.copyFileSync(source, dest);
|
|
151
152
|
console.log(`Copied skill ${fileName} into ${project}.`);
|
|
@@ -450,7 +451,7 @@ export function handleDetectSkills(args, profile) {
|
|
|
450
451
|
continue;
|
|
451
452
|
}
|
|
452
453
|
catch (err) {
|
|
453
|
-
if ((process.env.PHREN_DEBUG
|
|
454
|
+
if ((process.env.PHREN_DEBUG))
|
|
454
455
|
process.stderr.write(`[phren] skillList lstat: ${errorMessage(err)}\n`);
|
|
455
456
|
}
|
|
456
457
|
const name = entry.replace(/\.md$/, "");
|
|
@@ -548,6 +549,15 @@ function printSkillDoctor(scope, manifest, destDir) {
|
|
|
548
549
|
problems.push(`Mirror drift for ${skill.name}: expected ${dest} -> ${skill.root}`);
|
|
549
550
|
}
|
|
550
551
|
}
|
|
552
|
+
// Check for user-owned files blocking phren skill links
|
|
553
|
+
const phrenPath = getPhrenPath();
|
|
554
|
+
const srcDir = scope.toLowerCase() === "global"
|
|
555
|
+
? path.join(phrenPath, "global", "skills")
|
|
556
|
+
: path.join(phrenPath, scope, "skills");
|
|
557
|
+
const collisions = detectSkillCollisions(srcDir, destDir, phrenPath);
|
|
558
|
+
for (const collision of collisions) {
|
|
559
|
+
problems.push(`Skill collision: ${collision.message}`);
|
|
560
|
+
}
|
|
551
561
|
}
|
|
552
562
|
if (!manifest.problems.length && !problems.length) {
|
|
553
563
|
console.log("\nDoctor: no skill pipeline issues detected.");
|
|
@@ -853,7 +863,7 @@ function handleProjectsList(profile) {
|
|
|
853
863
|
dirFiles = new Set(fs.readdirSync(projectDir));
|
|
854
864
|
}
|
|
855
865
|
catch (err) {
|
|
856
|
-
if ((process.env.PHREN_DEBUG
|
|
866
|
+
if ((process.env.PHREN_DEBUG))
|
|
857
867
|
process.stderr.write(`[phren] projects list readdir: ${errorMessage(err)}\n`);
|
|
858
868
|
dirFiles = new Set();
|
|
859
869
|
}
|
|
@@ -896,7 +906,7 @@ async function handleProjectsRemove(name, profile) {
|
|
|
896
906
|
countFiles(projectDir);
|
|
897
907
|
}
|
|
898
908
|
catch (err) {
|
|
899
|
-
if ((process.env.PHREN_DEBUG
|
|
909
|
+
if ((process.env.PHREN_DEBUG))
|
|
900
910
|
process.stderr.write(`[phren] projects remove countFiles: ${errorMessage(err)}\n`);
|
|
901
911
|
}
|
|
902
912
|
const readline = await import("readline");
|
package/mcp/dist/cli-search.js
CHANGED
|
@@ -34,7 +34,7 @@ export function readSearchHistory(phrenPath) {
|
|
|
34
34
|
.map((line) => JSON.parse(line));
|
|
35
35
|
}
|
|
36
36
|
catch (err) {
|
|
37
|
-
if ((process.env.PHREN_DEBUG
|
|
37
|
+
if ((process.env.PHREN_DEBUG))
|
|
38
38
|
process.stderr.write(`[phren] readSearchHistory: ${errorMessage(err)}\n`);
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
@@ -253,7 +253,7 @@ export async function runSearch(opts, phrenPath, profile) {
|
|
|
253
253
|
logSearchMiss(phrenPath, opts.query, opts.project);
|
|
254
254
|
}
|
|
255
255
|
catch (err) {
|
|
256
|
-
if ((process.env.PHREN_DEBUG
|
|
256
|
+
if ((process.env.PHREN_DEBUG))
|
|
257
257
|
process.stderr.write(`[phren] search logSearchMiss: ${errorMessage(err)}\n`);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
@@ -107,7 +107,7 @@ function isAlreadyArchived(referenceDir, bullet) {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
catch (err) {
|
|
110
|
-
if ((process.env.PHREN_DEBUG
|
|
110
|
+
if ((process.env.PHREN_DEBUG))
|
|
111
111
|
process.stderr.write(`[phren] isDuplicateInReference: ${errorMessage(err)}\n`);
|
|
112
112
|
}
|
|
113
113
|
return false;
|
|
@@ -271,7 +271,7 @@ export function autoArchiveToReference(phrenPath, project, keepCount) {
|
|
|
271
271
|
fs.unlinkSync(lockFile);
|
|
272
272
|
}
|
|
273
273
|
catch (err) {
|
|
274
|
-
if ((process.env.PHREN_DEBUG
|
|
274
|
+
if ((process.env.PHREN_DEBUG))
|
|
275
275
|
process.stderr.write(`[phren] autoArchiveToReference unlockFile: ${errorMessage(err)}\n`);
|
|
276
276
|
}
|
|
277
277
|
}
|
|
@@ -5,7 +5,7 @@ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS } from "./shared.js";
|
|
|
5
5
|
import { errorMessage, runGitOrThrow } from "./utils.js";
|
|
6
6
|
import { findingIdFromLine } from "./finding-impact.js";
|
|
7
7
|
import { METADATA_REGEX, isArchiveStart, isArchiveEnd } from "./content-metadata.js";
|
|
8
|
-
import { FINDING_TYPE_DECAY, extractFindingType
|
|
8
|
+
import { FINDING_TYPE_DECAY, extractFindingType } from "./finding-lifecycle.js";
|
|
9
9
|
export const FINDING_PROVENANCE_SOURCES = [
|
|
10
10
|
"human",
|
|
11
11
|
"agent",
|
|
@@ -294,7 +294,6 @@ export function filterTrustedFindingsDetailed(content, opts) {
|
|
|
294
294
|
...(options.decay || {}),
|
|
295
295
|
};
|
|
296
296
|
const highImpactFindingIds = options.highImpactFindingIds;
|
|
297
|
-
const impactCounts = options.impactCounts;
|
|
298
297
|
const project = options.project;
|
|
299
298
|
const lines = content.split("\n");
|
|
300
299
|
const out = [];
|
|
@@ -413,29 +412,20 @@ export function filterTrustedFindingsDetailed(content, opts) {
|
|
|
413
412
|
confidence *= 0.9;
|
|
414
413
|
if (project && highImpactFindingIds?.size) {
|
|
415
414
|
const findingId = findingIdFromLine(line);
|
|
416
|
-
if (highImpactFindingIds.has(findingId))
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
|
|
428
|
-
}
|
|
415
|
+
if (highImpactFindingIds.has(findingId))
|
|
416
|
+
confidence *= 1.15;
|
|
417
|
+
}
|
|
418
|
+
// Confirmed findings decay 3x slower — recompute confidence with reduced age
|
|
419
|
+
{
|
|
420
|
+
const findingId = findingIdFromLine(line);
|
|
421
|
+
if (findingId && highImpactFindingIds?.has(findingId) && effectiveDate) {
|
|
422
|
+
const realAge = ageDaysForDate(effectiveDate);
|
|
423
|
+
if (realAge !== null) {
|
|
424
|
+
const slowedAge = Math.floor(realAge / 3);
|
|
425
|
+
confidence = Math.max(confidence, confidenceForAge(slowedAge, decay));
|
|
429
426
|
}
|
|
430
427
|
}
|
|
431
428
|
}
|
|
432
|
-
const lifecycle = parseFindingLifecycle(line);
|
|
433
|
-
if (lifecycle?.status === "superseded")
|
|
434
|
-
confidence *= 0.25;
|
|
435
|
-
if (lifecycle?.status === "retracted")
|
|
436
|
-
confidence *= 0.1;
|
|
437
|
-
if (lifecycle?.status === "contradicted")
|
|
438
|
-
confidence *= 0.4;
|
|
439
429
|
confidence = Math.max(0, Math.min(1, confidence));
|
|
440
430
|
if (confidence < minConfidence) {
|
|
441
431
|
issues.push({ date: effectiveDate || "unknown", bullet: line, reason: "stale" });
|
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
4
|
import { debugLog, runtimeFile, KNOWN_OBSERVATION_TAGS } from "./shared.js";
|
|
5
|
-
import { isFeatureEnabled, safeProjectPath } from "./utils.js";
|
|
5
|
+
import { isFeatureEnabled, safeProjectPath, errorMessage } from "./utils.js";
|
|
6
6
|
import { UNIVERSAL_TECH_TERMS_RE, EXTRA_ENTITY_PATTERNS } from "./phren-core.js";
|
|
7
7
|
import { isInactiveFindingLine } from "./finding-lifecycle.js";
|
|
8
8
|
// ── LLM provider abstraction ────────────────────────────────────────────────
|
|
@@ -50,8 +50,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
catch (err) {
|
|
53
|
-
if ((process.env.PHREN_DEBUG
|
|
54
|
-
process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${
|
|
53
|
+
if ((process.env.PHREN_DEBUG))
|
|
54
|
+
process.stderr.write(`[phren] withCache load (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
|
|
55
55
|
}
|
|
56
56
|
const result = await compute();
|
|
57
57
|
// Persist result
|
|
@@ -61,8 +61,8 @@ async function withCache(cachePath, key, ttlMs, compute) {
|
|
|
61
61
|
persistCache(cachePath, cache);
|
|
62
62
|
}
|
|
63
63
|
catch (err) {
|
|
64
|
-
if ((process.env.PHREN_DEBUG
|
|
65
|
-
process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${
|
|
64
|
+
if ((process.env.PHREN_DEBUG))
|
|
65
|
+
process.stderr.write(`[phren] withCache persist (${path.basename(cachePath)}): ${errorMessage(err)}\n`);
|
|
66
66
|
}
|
|
67
67
|
return result;
|
|
68
68
|
}
|
|
@@ -562,8 +562,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
|
|
|
562
562
|
return { name: e.name, mtime: fs.statSync(fp).mtimeMs, fp };
|
|
563
563
|
}
|
|
564
564
|
catch (err) {
|
|
565
|
-
if ((process.env.PHREN_DEBUG
|
|
566
|
-
process.stderr.write(`[phren] crossProjectScan stat: ${
|
|
565
|
+
if ((process.env.PHREN_DEBUG))
|
|
566
|
+
process.stderr.write(`[phren] crossProjectScan stat: ${errorMessage(err)}\n`);
|
|
567
567
|
return null;
|
|
568
568
|
}
|
|
569
569
|
})
|
|
@@ -577,8 +577,8 @@ export async function checkSemanticConflicts(phrenPath, project, newFinding, sig
|
|
|
577
577
|
}
|
|
578
578
|
}
|
|
579
579
|
catch (err) {
|
|
580
|
-
if ((process.env.PHREN_DEBUG
|
|
581
|
-
process.stderr.write(`[phren] crossProjectScan: ${
|
|
580
|
+
if ((process.env.PHREN_DEBUG))
|
|
581
|
+
process.stderr.write(`[phren] crossProjectScan: ${errorMessage(err)}\n`);
|
|
582
582
|
}
|
|
583
583
|
const annotations = [];
|
|
584
584
|
const deadline = Date.now() + CONFLICT_CHECK_TOTAL_TIMEOUT_MS;
|
package/mcp/dist/data-access.js
CHANGED
|
@@ -8,7 +8,7 @@ import { isValidProjectName, queueFilePath, safeProjectPath, errorMessage } from
|
|
|
8
8
|
import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
|
|
9
9
|
import { parseFindingLifecycle, } from "./finding-lifecycle.js";
|
|
10
10
|
import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
|
|
11
|
-
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
11
|
+
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, removeTasks, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
12
12
|
export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
|
|
13
13
|
export { loadShellState, readRuntimeHealth, resetShellState, saveShellState, } from "./shell-state-store.js";
|
|
14
14
|
function withSafeLock(filePath, fn) {
|
package/mcp/dist/data-tasks.js
CHANGED
|
@@ -528,6 +528,29 @@ export function removeTask(phrenPath, project, match) {
|
|
|
528
528
|
return phrenOk(`Removed task from ${project}: ${item.line}`);
|
|
529
529
|
});
|
|
530
530
|
}
|
|
531
|
+
export function removeTasks(phrenPath, project, matches) {
|
|
532
|
+
const bPath = canonicalTaskFilePath(phrenPath, project);
|
|
533
|
+
if (!bPath)
|
|
534
|
+
return phrenErr(`Project name "${project}" is not valid.`, PhrenError.INVALID_PROJECT_NAME);
|
|
535
|
+
return withSafeLock(bPath, () => {
|
|
536
|
+
const parsed = readTasks(phrenPath, project);
|
|
537
|
+
if (!parsed.ok)
|
|
538
|
+
return forwardErr(parsed);
|
|
539
|
+
const removed = [];
|
|
540
|
+
const errors = [];
|
|
541
|
+
for (const match of matches) {
|
|
542
|
+
const found = findItemByMatch(parsed.data, match);
|
|
543
|
+
if (found.error || !found.match) {
|
|
544
|
+
errors.push(match);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const [item] = parsed.data.items[found.match.section].splice(found.match.index, 1);
|
|
548
|
+
removed.push(item.line);
|
|
549
|
+
}
|
|
550
|
+
writeTaskDoc(parsed.data);
|
|
551
|
+
return phrenOk({ removed, errors });
|
|
552
|
+
});
|
|
553
|
+
}
|
|
531
554
|
export function updateTask(phrenPath, project, match, updates) {
|
|
532
555
|
const bPath = canonicalTaskFilePath(phrenPath, project);
|
|
533
556
|
if (!bPath)
|