@phren/cli 0.0.32 → 0.0.34
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/cli/actions.js +3 -0
- package/mcp/dist/cli/config.js +3 -3
- package/mcp/dist/cli/govern.js +18 -8
- package/mcp/dist/cli/hooks-context.js +1 -1
- package/mcp/dist/cli/hooks-session.js +18 -62
- package/mcp/dist/cli/namespaces.js +1 -1
- package/mcp/dist/cli/search.js +5 -5
- package/mcp/dist/cli-hooks-prompt.js +7 -3
- package/mcp/dist/cli-hooks-session-handlers.js +3 -15
- package/mcp/dist/cli-hooks-stop.js +10 -48
- package/mcp/dist/content/archive.js +8 -20
- package/mcp/dist/content/learning.js +29 -8
- package/mcp/dist/data/access.js +13 -4
- package/mcp/dist/finding/lifecycle.js +9 -3
- package/mcp/dist/governance/audit.js +13 -5
- package/mcp/dist/governance/policy.js +13 -0
- package/mcp/dist/governance/rbac.js +1 -1
- package/mcp/dist/governance/scores.js +2 -1
- package/mcp/dist/hooks.js +52 -6
- package/mcp/dist/index.js +1 -1
- package/mcp/dist/init/init.js +66 -45
- package/mcp/dist/init/shared.js +1 -1
- package/mcp/dist/init-bootstrap.js +0 -47
- package/mcp/dist/init-fresh.js +13 -18
- package/mcp/dist/init-uninstall.js +22 -0
- package/mcp/dist/init-walkthrough.js +19 -24
- package/mcp/dist/link/doctor.js +9 -0
- package/mcp/dist/package-metadata.js +1 -1
- package/mcp/dist/phren-art.js +4 -120
- package/mcp/dist/proactivity.js +1 -1
- package/mcp/dist/project-topics.js +16 -46
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/runtime-profile.js +1 -1
- package/mcp/dist/shared/data-utils.js +25 -0
- package/mcp/dist/shared/fragment-graph.js +4 -18
- package/mcp/dist/shared/index.js +14 -10
- package/mcp/dist/shared/ollama.js +23 -5
- package/mcp/dist/shared/process.js +24 -0
- package/mcp/dist/shared/retrieval.js +7 -4
- package/mcp/dist/shared/search-fallback.js +1 -0
- package/mcp/dist/shared.js +2 -1
- package/mcp/dist/shell/render.js +1 -1
- package/mcp/dist/skill/registry.js +1 -1
- package/mcp/dist/skill/state.js +0 -3
- package/mcp/dist/task/github.js +1 -0
- package/mcp/dist/task/lifecycle.js +1 -6
- package/mcp/dist/tools/config.js +415 -400
- package/mcp/dist/tools/finding.js +390 -373
- package/mcp/dist/tools/ops.js +372 -365
- package/mcp/dist/tools/search.js +495 -487
- package/mcp/dist/tools/session.js +3 -2
- package/mcp/dist/tools/skills.js +9 -0
- package/mcp/dist/ui/page.js +1 -1
- package/mcp/dist/ui/server.js +645 -1040
- package/mcp/dist/utils.js +12 -8
- package/package.json +1 -1
- package/mcp/dist/init-dryrun.js +0 -55
- package/mcp/dist/init-migrate.js +0 -51
- package/mcp/dist/init-walkthrough-merge.js +0 -90
package/mcp/dist/cli/actions.js
CHANGED
|
@@ -290,6 +290,9 @@ export async function handleUpdate(args) {
|
|
|
290
290
|
if (!result.ok) {
|
|
291
291
|
process.exitCode = 1;
|
|
292
292
|
}
|
|
293
|
+
else {
|
|
294
|
+
console.log("Run 'npx phren init' to refresh hooks and config.");
|
|
295
|
+
}
|
|
293
296
|
}
|
|
294
297
|
export async function handleReview(args) {
|
|
295
298
|
const phrenPath = getPhrenPath();
|
package/mcp/dist/cli/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForTask, ge
|
|
|
8
8
|
import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, updateProjectConfigOverrides, } from "../project-config.js";
|
|
9
9
|
import { isValidProjectName, learnedSynonymsPath, learnSynonym, loadLearnedSynonyms, removeLearnedSynonym, } from "../utils.js";
|
|
10
10
|
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
11
|
+
function parseProjectArg(args) {
|
|
12
12
|
const project = args.find((a) => a.startsWith("--project="))?.slice("--project=".length)
|
|
13
13
|
?? (args.indexOf("--project") !== -1 ? args[args.indexOf("--project") + 1] : undefined);
|
|
14
14
|
const rest = args.filter((a, i) => a !== "--project" && !a.startsWith("--project=") && args[i - 1] !== "--project");
|
|
@@ -53,7 +53,7 @@ function formatConfigAsTable(label, rows) {
|
|
|
53
53
|
console.log(` ${k.padEnd(maxKey)} ${v}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
function handleConfigShow(args) {
|
|
57
57
|
const phrenPath = getPhrenPath();
|
|
58
58
|
const { project: projectArg } = parseProjectArg(args);
|
|
59
59
|
if (projectArg) {
|
|
@@ -461,7 +461,7 @@ function handleConfigFindingSensitivity(args) {
|
|
|
461
461
|
// ── LLM config ───────────────────────────────────────────────────────────────
|
|
462
462
|
const EXPENSIVE_MODEL_RE = /opus|sonnet|gpt-4(?!o-mini)/i;
|
|
463
463
|
const DEFAULT_LLM_MODEL = "gpt-4o-mini / claude-haiku-4-5-20251001";
|
|
464
|
-
|
|
464
|
+
function printSemanticCostNotice(model) {
|
|
465
465
|
const effectiveModel = model || process.env.PHREN_LLM_MODEL || DEFAULT_LLM_MODEL;
|
|
466
466
|
console.log(` Note: Each semantic check is ~80 input + ~5 output tokens (one call per 'maybe' pair, cached 24h).`);
|
|
467
467
|
console.log(` Current model: ${effectiveModel}`);
|
package/mcp/dist/cli/govern.js
CHANGED
|
@@ -228,10 +228,10 @@ export async function handleConsolidateMemories(args = []) {
|
|
|
228
228
|
return;
|
|
229
229
|
console.log(`Updated backups (${backups.length}): ${backups.join(", ")}`);
|
|
230
230
|
}
|
|
231
|
-
|
|
231
|
+
async function handleGcMaintain(args = []) {
|
|
232
232
|
const dryRun = args.includes("--dry-run");
|
|
233
233
|
const phrenPath = getPhrenPath();
|
|
234
|
-
const {
|
|
234
|
+
const { execFileSync } = await import("child_process");
|
|
235
235
|
const report = {
|
|
236
236
|
gitGcRan: false,
|
|
237
237
|
commitsSquashed: 0,
|
|
@@ -244,7 +244,7 @@ export async function handleGcMaintain(args = []) {
|
|
|
244
244
|
}
|
|
245
245
|
else {
|
|
246
246
|
try {
|
|
247
|
-
|
|
247
|
+
execFileSync("git", ["gc", "--aggressive", "--quiet"], { cwd: phrenPath, stdio: "pipe" });
|
|
248
248
|
report.gitGcRan = true;
|
|
249
249
|
console.log("git gc --aggressive: done");
|
|
250
250
|
}
|
|
@@ -256,7 +256,10 @@ export async function handleGcMaintain(args = []) {
|
|
|
256
256
|
const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);
|
|
257
257
|
let oldCommits = [];
|
|
258
258
|
try {
|
|
259
|
-
const raw =
|
|
259
|
+
const raw = execFileSync("git", ["log", `--before=${sevenDaysAgo}`, "--format=%H %ci %s"], {
|
|
260
|
+
cwd: phrenPath,
|
|
261
|
+
encoding: "utf8",
|
|
262
|
+
}).trim();
|
|
260
263
|
if (raw) {
|
|
261
264
|
oldCommits = raw.split("\n").filter((l) => l.includes("auto-save:") || l.includes("[auto]"));
|
|
262
265
|
}
|
|
@@ -295,8 +298,11 @@ export async function handleGcMaintain(args = []) {
|
|
|
295
298
|
try {
|
|
296
299
|
const oldest = hashes[hashes.length - 1];
|
|
297
300
|
const newest = hashes[0];
|
|
298
|
-
// Use git
|
|
299
|
-
const parentOfOldest =
|
|
301
|
+
// Use git rev-parse to get the parent of the oldest commit
|
|
302
|
+
const parentOfOldest = execFileSync("git", ["rev-parse", `${oldest}^`], {
|
|
303
|
+
cwd: phrenPath,
|
|
304
|
+
encoding: "utf8",
|
|
305
|
+
}).trim();
|
|
300
306
|
// Build rebase script via env variable to squash all but first to "squash"
|
|
301
307
|
const rebaseScript = hashes
|
|
302
308
|
.map((h, i) => `${i === hashes.length - 1 ? "pick" : "squash"} ${h} auto-save`)
|
|
@@ -304,8 +310,12 @@ export async function handleGcMaintain(args = []) {
|
|
|
304
310
|
.join("\n");
|
|
305
311
|
const scriptPath = path.join(phrenPath, ".runtime", `gc-rebase-${weekKey}.sh`);
|
|
306
312
|
fs.writeFileSync(scriptPath, rebaseScript);
|
|
307
|
-
// Use GIT_SEQUENCE_EDITOR to feed our script
|
|
308
|
-
|
|
313
|
+
// Use GIT_SEQUENCE_EDITOR env var to feed our script to git rebase
|
|
314
|
+
execFileSync("git", ["rebase", "-i", parentOfOldest], {
|
|
315
|
+
cwd: phrenPath,
|
|
316
|
+
stdio: "pipe",
|
|
317
|
+
env: { ...process.env, GIT_SEQUENCE_EDITOR: `cat ${scriptPath} >` },
|
|
318
|
+
});
|
|
309
319
|
fs.unlinkSync(scriptPath);
|
|
310
320
|
report.commitsSquashed += hashes.length - 1;
|
|
311
321
|
console.log(`Squashed ${hashes.length} auto-save commits for week of ${weekKey} (${newest.slice(0, 7)}..${oldest.slice(0, 7)}).`);
|
|
@@ -39,7 +39,7 @@ export function handleGuardSkip(ctx, hookName, reason, healthUpdate) {
|
|
|
39
39
|
}
|
|
40
40
|
// Re-export frequently used functions so hook handlers can import from one place
|
|
41
41
|
export { debugLog, appendAuditLog, getPhrenPath, readRootManifest, sessionMarker, runtimeFile, EXEC_TIMEOUT_MS, getProjectDirs, findProjectNameCaseInsensitive, homePath, } from "../shared.js";
|
|
42
|
-
export { updateRuntimeHealth, getWorkflowPolicy, withFileLock, appendReviewQueue, recordFeedback, getQualityMultiplier, } from "../shared/governance.js";
|
|
42
|
+
export { updateRuntimeHealth, buildSyncStatus, getWorkflowPolicy, withFileLock, appendReviewQueue, recordFeedback, getQualityMultiplier, } from "../shared/governance.js";
|
|
43
43
|
export { detectProject } from "../shared/index.js";
|
|
44
44
|
export { isProjectHookEnabled, readProjectConfig, getProjectSourcePath } from "../project-config.js";
|
|
45
45
|
export { resolveRuntimeProfile } from "../runtime-profile.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildHookContext, handleGuardSkip, debugLog, appendAuditLog, runtimeFile, sessionMarker, EXEC_TIMEOUT_MS, getPhrenPath, getProjectDirs, findProjectNameCaseInsensitive, homePath, updateRuntimeHealth, withFileLock, getWorkflowPolicy, appendReviewQueue, recordFeedback, getQualityMultiplier, detectProject, isProjectHookEnabled, readProjectConfig, getProjectSourcePath, detectProjectDir, ensureLocalGitRepo, isProjectTracked, repairPreexistingInstall, getProactivityLevelForTask, getProactivityLevelForFindings, hasExplicitFindingSignal, shouldAutoCaptureFindingsForLevel, FINDING_SENSITIVITY_CONFIG, isFeatureEnabled, errorMessage, bootstrapPhrenDotEnv, finalizeTaskSession, appendFindingJournal, runDoctor, resolveRuntimeProfile, } from "./hooks-context.js";
|
|
1
|
+
import { buildHookContext, handleGuardSkip, debugLog, appendAuditLog, runtimeFile, sessionMarker, EXEC_TIMEOUT_MS, getPhrenPath, getProjectDirs, findProjectNameCaseInsensitive, homePath, updateRuntimeHealth, withFileLock, getWorkflowPolicy, appendReviewQueue, recordFeedback, getQualityMultiplier, detectProject, isProjectHookEnabled, readProjectConfig, getProjectSourcePath, detectProjectDir, ensureLocalGitRepo, isProjectTracked, repairPreexistingInstall, getProactivityLevelForTask, getProactivityLevelForFindings, hasExplicitFindingSignal, shouldAutoCaptureFindingsForLevel, FINDING_SENSITIVITY_CONFIG, isFeatureEnabled, errorMessage, bootstrapPhrenDotEnv, finalizeTaskSession, appendFindingJournal, runDoctor, resolveRuntimeProfile, buildSyncStatus, } from "./hooks-context.js";
|
|
2
2
|
import { sessionMetricsFile, qualityMarkers, } from "../shared.js";
|
|
3
3
|
import { autoMergeConflicts, mergeTask, mergeFindings, } from "../shared/content.js";
|
|
4
4
|
import { runGit } from "../utils.js";
|
|
@@ -6,16 +6,16 @@ import { readInstallPreferences } from "../init/preferences.js";
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
|
-
import { execFileSync
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
10
|
+
import { spawnDetachedChild } from "../shared/process.js";
|
|
10
11
|
import { fileURLToPath } from "url";
|
|
11
12
|
import { isTaskFileName, TASKS_FILENAME } from "../data/tasks.js";
|
|
12
13
|
import { buildIndex, queryRows, } from "../shared/index.js";
|
|
13
14
|
import { filterTaskByPriority } from "../shared/retrieval.js";
|
|
14
15
|
import { logger } from "../logger.js";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
export { buildHookContext, checkHookGuard, handleGuardSkip } from "./hooks-context.js";
|
|
16
|
+
const SYNC_LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes
|
|
17
|
+
const MAINTENANCE_LOCK_STALE_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
18
|
+
export { buildHookContext, handleGuardSkip } from "./hooks-context.js";
|
|
19
19
|
/** Read JSON from stdin if it's not a TTY. Returns null if stdin is a TTY or parsing fails. */
|
|
20
20
|
function readStdinJson() {
|
|
21
21
|
if (process.stdin.isTTY)
|
|
@@ -252,7 +252,7 @@ function scheduleBackgroundSync(phrenPathLocal) {
|
|
|
252
252
|
try {
|
|
253
253
|
if (fs.existsSync(lockPath)) {
|
|
254
254
|
const ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
255
|
-
if (ageMs <=
|
|
255
|
+
if (ageMs <= SYNC_LOCK_STALE_MS)
|
|
256
256
|
return false;
|
|
257
257
|
fs.unlinkSync(lockPath);
|
|
258
258
|
}
|
|
@@ -265,16 +265,7 @@ function scheduleBackgroundSync(phrenPathLocal) {
|
|
|
265
265
|
fs.writeFileSync(lockPath, JSON.stringify({ startedAt: new Date().toISOString(), pid: process.pid }) + "\n", { flag: "wx" });
|
|
266
266
|
const logFd = fs.openSync(logPath, "a");
|
|
267
267
|
fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
|
|
268
|
-
const child =
|
|
269
|
-
cwd: process.cwd(),
|
|
270
|
-
detached: true,
|
|
271
|
-
stdio: ["ignore", logFd, logFd],
|
|
272
|
-
env: {
|
|
273
|
-
...process.env,
|
|
274
|
-
PHREN_PATH: phrenPathLocal,
|
|
275
|
-
PHREN_PROFILE: getRuntimeProfile(),
|
|
276
|
-
},
|
|
277
|
-
});
|
|
268
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: phrenPathLocal, logFd });
|
|
278
269
|
child.unref();
|
|
279
270
|
fs.closeSync(logFd);
|
|
280
271
|
return true;
|
|
@@ -297,7 +288,7 @@ function scheduleBackgroundMaintenance(phrenPathLocal, project) {
|
|
|
297
288
|
if (fs.existsSync(markers.lock)) {
|
|
298
289
|
try {
|
|
299
290
|
const ageMs = Date.now() - fs.statSync(markers.lock).mtimeMs;
|
|
300
|
-
if (ageMs <=
|
|
291
|
+
if (ageMs <= MAINTENANCE_LOCK_STALE_MS)
|
|
301
292
|
return false;
|
|
302
293
|
fs.unlinkSync(markers.lock);
|
|
303
294
|
}
|
|
@@ -339,16 +330,7 @@ function scheduleBackgroundMaintenance(phrenPathLocal, project) {
|
|
|
339
330
|
const logPath = path.join(logDir, "background-maintenance.log");
|
|
340
331
|
const logFd = fs.openSync(logPath, "a");
|
|
341
332
|
fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
|
|
342
|
-
const child =
|
|
343
|
-
cwd: process.cwd(),
|
|
344
|
-
detached: true,
|
|
345
|
-
stdio: ["ignore", logFd, logFd],
|
|
346
|
-
env: {
|
|
347
|
-
...process.env,
|
|
348
|
-
PHREN_PATH: phrenPathLocal,
|
|
349
|
-
PHREN_PROFILE: getRuntimeProfile(),
|
|
350
|
-
},
|
|
351
|
-
});
|
|
333
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: phrenPathLocal, logFd });
|
|
352
334
|
child.on("exit", (code, signal) => {
|
|
353
335
|
const msg = `[${new Date().toISOString()}] exit code=${code ?? "null"} signal=${signal ?? "none"}\n`;
|
|
354
336
|
try {
|
|
@@ -1019,12 +1001,7 @@ export async function handleBackgroundSync() {
|
|
|
1019
1001
|
const unsyncedCommits = await countUnsyncedCommits(phrenPathLocal);
|
|
1020
1002
|
updateRuntimeHealth(phrenPathLocal, {
|
|
1021
1003
|
lastAutoSave: { at: now, status: "saved-local", detail: "background sync skipped; no remote configured" },
|
|
1022
|
-
lastSync: {
|
|
1023
|
-
lastPushAt: now,
|
|
1024
|
-
lastPushStatus: "saved-local",
|
|
1025
|
-
lastPushDetail: "background sync skipped; no remote configured",
|
|
1026
|
-
unsyncedCommits,
|
|
1027
|
-
},
|
|
1004
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-local", pushDetail: "background sync skipped; no remote configured", unsyncedCommits }),
|
|
1028
1005
|
});
|
|
1029
1006
|
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-local detail=no_remote");
|
|
1030
1007
|
return;
|
|
@@ -1033,12 +1010,7 @@ export async function handleBackgroundSync() {
|
|
|
1033
1010
|
if (push.ok) {
|
|
1034
1011
|
updateRuntimeHealth(phrenPathLocal, {
|
|
1035
1012
|
lastAutoSave: { at: now, status: "saved-pushed", detail: "commit pushed by background sync" },
|
|
1036
|
-
lastSync: {
|
|
1037
|
-
lastPushAt: now,
|
|
1038
|
-
lastPushStatus: "saved-pushed",
|
|
1039
|
-
lastPushDetail: "commit pushed by background sync",
|
|
1040
|
-
unsyncedCommits: 0,
|
|
1041
|
-
},
|
|
1013
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-pushed", pushDetail: "commit pushed by background sync", unsyncedCommits: 0 }),
|
|
1042
1014
|
});
|
|
1043
1015
|
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-pushed");
|
|
1044
1016
|
return;
|
|
@@ -1047,34 +1019,18 @@ export async function handleBackgroundSync() {
|
|
|
1047
1019
|
if (recovered.ok) {
|
|
1048
1020
|
updateRuntimeHealth(phrenPathLocal, {
|
|
1049
1021
|
lastAutoSave: { at: now, status: "saved-pushed", detail: recovered.detail },
|
|
1050
|
-
lastSync: {
|
|
1051
|
-
lastPullAt: now,
|
|
1052
|
-
lastPullStatus: recovered.pullStatus,
|
|
1053
|
-
lastPullDetail: recovered.pullDetail,
|
|
1054
|
-
lastSuccessfulPullAt: now,
|
|
1055
|
-
lastPushAt: now,
|
|
1056
|
-
lastPushStatus: "saved-pushed",
|
|
1057
|
-
lastPushDetail: recovered.detail,
|
|
1058
|
-
unsyncedCommits: 0,
|
|
1059
|
-
},
|
|
1022
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-pushed", pushDetail: recovered.detail, pullAt: now, pullStatus: recovered.pullStatus, pullDetail: recovered.pullDetail, successfulPullAt: now, unsyncedCommits: 0 }),
|
|
1060
1023
|
});
|
|
1061
1024
|
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-pushed detail=${JSON.stringify(recovered.detail)}`);
|
|
1062
1025
|
return;
|
|
1063
1026
|
}
|
|
1064
1027
|
const unsyncedCommits = await countUnsyncedCommits(phrenPathLocal);
|
|
1028
|
+
const failDetail = recovered.detail || push.error || "background sync push failed";
|
|
1065
1029
|
updateRuntimeHealth(phrenPathLocal, {
|
|
1066
|
-
lastAutoSave: { at: now, status: "saved-local", detail:
|
|
1067
|
-
lastSync: {
|
|
1068
|
-
lastPullAt: now,
|
|
1069
|
-
lastPullStatus: recovered.pullStatus,
|
|
1070
|
-
lastPullDetail: recovered.pullDetail,
|
|
1071
|
-
lastPushAt: now,
|
|
1072
|
-
lastPushStatus: "saved-local",
|
|
1073
|
-
lastPushDetail: recovered.detail || push.error || "background sync push failed",
|
|
1074
|
-
unsyncedCommits,
|
|
1075
|
-
},
|
|
1030
|
+
lastAutoSave: { at: now, status: "saved-local", detail: failDetail },
|
|
1031
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-local", pushDetail: failDetail, pullAt: now, pullStatus: recovered.pullStatus, pullDetail: recovered.pullDetail, unsyncedCommits }),
|
|
1076
1032
|
});
|
|
1077
|
-
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-local detail=${JSON.stringify(
|
|
1033
|
+
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-local detail=${JSON.stringify(failDetail)}`);
|
|
1078
1034
|
}
|
|
1079
1035
|
finally {
|
|
1080
1036
|
try {
|
|
@@ -1091,7 +1047,7 @@ function scheduleWeeklyGovernance() {
|
|
|
1091
1047
|
if (daysSince >= 7) {
|
|
1092
1048
|
const spawnArgs = resolveSubprocessArgs("background-maintenance");
|
|
1093
1049
|
if (spawnArgs) {
|
|
1094
|
-
const child =
|
|
1050
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: getPhrenPath() });
|
|
1095
1051
|
child.unref();
|
|
1096
1052
|
fs.writeFileSync(lastGovPath, Date.now().toString());
|
|
1097
1053
|
debugLog("hook_stop: scheduled weekly governance run");
|
|
@@ -667,7 +667,7 @@ export async function handleProjectsNamespace(args, profile) {
|
|
|
667
667
|
console.error(`Project "${name}" not found.`);
|
|
668
668
|
process.exit(1);
|
|
669
669
|
}
|
|
670
|
-
const { readFindings, readTasks, resolveTaskFilePath
|
|
670
|
+
const { readFindings, readTasks, resolveTaskFilePath } = await import("../data/access.js");
|
|
671
671
|
const exported = { project: name, exportedAt: new Date().toISOString(), version: 1 };
|
|
672
672
|
const summaryPath = path.join(projectDir, "summary.md");
|
|
673
673
|
if (fs.existsSync(summaryPath))
|
package/mcp/dist/cli/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { runtimeFile } from "../shared.js";
|
|
4
|
-
import { buildIndex, extractSnippet, queryDocRows, queryRows,
|
|
4
|
+
import { buildIndex, extractSnippet, queryDocRows, queryRows, queryFragmentLinks, queryDocBySourceKey, logFragmentMiss } from "../shared/index.js";
|
|
5
5
|
import { buildFtsQueryVariants, errorMessage, isValidProjectName } from "../utils.js";
|
|
6
6
|
import { logger } from "../logger.js";
|
|
7
7
|
import { keywordFallbackSearch } from "../core/search.js";
|
|
@@ -24,7 +24,7 @@ const SEARCH_TYPES = new Set([
|
|
|
24
24
|
function historyFile(phrenPath) {
|
|
25
25
|
return runtimeFile(phrenPath, "search-history.jsonl");
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
function readSearchHistory(phrenPath) {
|
|
28
28
|
const file = historyFile(phrenPath);
|
|
29
29
|
if (!fs.existsSync(file))
|
|
30
30
|
return [];
|
|
@@ -319,7 +319,7 @@ export async function runFragmentSearch(query, phrenPath, profile, opts) {
|
|
|
319
319
|
}
|
|
320
320
|
const rows = queryRows(db, sql, params);
|
|
321
321
|
if (!rows || rows.length === 0) {
|
|
322
|
-
|
|
322
|
+
logFragmentMiss(phrenPath, query, "cli_search_fragments", opts.project);
|
|
323
323
|
return { lines: [`No fragments matching "${query}".`], exitCode: 0 };
|
|
324
324
|
}
|
|
325
325
|
const lines = [`Fragments matching "${query}" (${rows.length} result(s)):\n`];
|
|
@@ -362,14 +362,14 @@ export async function runRelatedDocs(entity, phrenPath, profile, opts) {
|
|
|
362
362
|
}
|
|
363
363
|
const db = await buildIndex(phrenPath, profile);
|
|
364
364
|
const max = opts.limit ?? 10;
|
|
365
|
-
const links =
|
|
365
|
+
const links = queryFragmentLinks(db, entity.toLowerCase());
|
|
366
366
|
let relatedDocs = links.related.filter(r => r.includes("/"));
|
|
367
367
|
if (opts.project) {
|
|
368
368
|
relatedDocs = relatedDocs.filter(d => d.startsWith(`${opts.project}/`));
|
|
369
369
|
}
|
|
370
370
|
relatedDocs = relatedDocs.slice(0, max);
|
|
371
371
|
if (relatedDocs.length === 0) {
|
|
372
|
-
|
|
372
|
+
logFragmentMiss(phrenPath, entity, "cli_related_docs", opts.project);
|
|
373
373
|
return { lines: [`No docs found referencing fragment "${entity}".`], exitCode: 0 };
|
|
374
374
|
}
|
|
375
375
|
const lines = [`Docs referencing "${entity}" (${relatedDocs.length} result(s)):\n`];
|
|
@@ -36,7 +36,10 @@ export async function handleHookContext() {
|
|
|
36
36
|
const findingsRow = queryRows(db, "SELECT content FROM docs WHERE project = ? AND type = 'findings'", [project]);
|
|
37
37
|
if (findingsRow) {
|
|
38
38
|
const content = findingsRow[0][0];
|
|
39
|
-
const bullets = content.split("\n")
|
|
39
|
+
const bullets = content.split("\n")
|
|
40
|
+
.filter(l => l.startsWith("- "))
|
|
41
|
+
.filter(l => !(/<!--\s*superseded_by:/.test(l) || /<!--\s*(?:phren:)?retract/.test(l) || /status\s+"retracted"/.test(l) || /status\s+"superseded"/.test(l)))
|
|
42
|
+
.slice(0, 10);
|
|
40
43
|
if (bullets.length > 0) {
|
|
41
44
|
parts.push("## Recent findings");
|
|
42
45
|
parts.push(bullets.join("\n"));
|
|
@@ -72,6 +75,7 @@ export async function handleHookContext() {
|
|
|
72
75
|
// ── PostToolUse hook ─────────────────────────────────────────────────────────
|
|
73
76
|
const INTERESTING_TOOLS = new Set(["Read", "Write", "Edit", "Bash", "Glob", "Grep"]);
|
|
74
77
|
const COOLDOWN_MS = parseInt(process.env.PHREN_AUTOCAPTURE_COOLDOWN_MS ?? "30000", 10);
|
|
78
|
+
const MAX_TOOL_COMMAND_LENGTH = 200;
|
|
75
79
|
function flattenToolResponseText(value, maxChars = 4000) {
|
|
76
80
|
if (typeof value === "string")
|
|
77
81
|
return value;
|
|
@@ -144,7 +148,7 @@ export async function handleHookTool() {
|
|
|
144
148
|
else if (toolName === "Bash") {
|
|
145
149
|
const cmd = input.command ?? undefined;
|
|
146
150
|
if (cmd)
|
|
147
|
-
entry.command = String(cmd).slice(0,
|
|
151
|
+
entry.command = String(cmd).slice(0, MAX_TOOL_COMMAND_LENGTH);
|
|
148
152
|
}
|
|
149
153
|
else if (toolName === "Glob") {
|
|
150
154
|
const pattern = input.pattern ?? undefined;
|
|
@@ -155,7 +159,7 @@ export async function handleHookTool() {
|
|
|
155
159
|
const pattern = input.pattern ?? undefined;
|
|
156
160
|
const searchPath = input.path ?? undefined;
|
|
157
161
|
if (pattern)
|
|
158
|
-
entry.command = `grep ${pattern}${searchPath ? ` in ${searchPath}` : ""}`.slice(0,
|
|
162
|
+
entry.command = `grep ${pattern}${searchPath ? ` in ${searchPath}` : ""}`.slice(0, MAX_TOOL_COMMAND_LENGTH);
|
|
159
163
|
}
|
|
160
164
|
const responseStr = flattenToolResponseText(data.tool_response ?? "");
|
|
161
165
|
if (/(error|exception|failed|no such file|ENOENT)/i.test(responseStr)) {
|
|
@@ -2,18 +2,15 @@
|
|
|
2
2
|
* SessionStart handler and onboarding helpers.
|
|
3
3
|
* Extracted from cli-hooks-session.ts for modularity.
|
|
4
4
|
*/
|
|
5
|
-
import { buildHookContext, handleGuardSkip, debugLog, sessionMarker,
|
|
5
|
+
import { buildHookContext, handleGuardSkip, debugLog, sessionMarker, getProjectDirs, findProjectNameCaseInsensitive, updateRuntimeHealth, appendAuditLog, detectProject, isProjectHookEnabled, readProjectConfig, getProjectSourcePath, detectProjectDir, ensureLocalGitRepo, isProjectTracked, repairPreexistingInstall, isFeatureEnabled, errorMessage, runDoctor, resolveRuntimeProfile, } from "./cli/hooks-context.js";
|
|
6
6
|
import { qualityMarkers, } from "./shared.js";
|
|
7
7
|
import { readInstallPreferences } from "./init/preferences.js";
|
|
8
8
|
import { logger } from "./logger.js";
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
-
import {
|
|
11
|
+
import { spawnDetachedChild } from "./shared/process.js";
|
|
12
12
|
import { TASKS_FILENAME } from "./data/tasks.js";
|
|
13
13
|
import { resolveSubprocessArgs as _resolveSubprocessArgs, runBestEffortGit, countUnsyncedCommits, } from "./cli-hooks-git.js";
|
|
14
|
-
function getRuntimeProfile() {
|
|
15
|
-
return resolveRuntimeProfile(getPhrenPath());
|
|
16
|
-
}
|
|
17
14
|
const SESSION_START_ONBOARDING_MARKER = "session-start-onboarding-v1";
|
|
18
15
|
const SYNC_WARN_MARKER = "sync-broken-warned-v1";
|
|
19
16
|
function projectHasBootstrapSignals(phrenPath, project) {
|
|
@@ -156,16 +153,7 @@ function scheduleBackgroundMaintenance(phrenPathLocal, project) {
|
|
|
156
153
|
const logPath = path.join(logDir, "background-maintenance.log");
|
|
157
154
|
const logFd = fs.openSync(logPath, "a");
|
|
158
155
|
fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
|
|
159
|
-
const child =
|
|
160
|
-
cwd: process.cwd(),
|
|
161
|
-
detached: true,
|
|
162
|
-
stdio: ["ignore", logFd, logFd],
|
|
163
|
-
env: {
|
|
164
|
-
...process.env,
|
|
165
|
-
PHREN_PATH: phrenPathLocal,
|
|
166
|
-
PHREN_PROFILE: getRuntimeProfile(),
|
|
167
|
-
},
|
|
168
|
-
});
|
|
156
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: phrenPathLocal, logFd });
|
|
169
157
|
child.on("exit", (code, signal) => {
|
|
170
158
|
const msg = `[${new Date().toISOString()}] exit code=${code ?? "null"} signal=${signal ?? "none"}\n`;
|
|
171
159
|
try {
|
|
@@ -2,16 +2,14 @@
|
|
|
2
2
|
* Stop hook handler: git commit/push, background sync, auto-capture, governance.
|
|
3
3
|
* Extracted from cli-hooks-session.ts for modularity.
|
|
4
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,
|
|
5
|
+
import { buildHookContext, handleGuardSkip, debugLog, runtimeFile, sessionMarker, getPhrenPath, updateRuntimeHealth, buildSyncStatus, appendAuditLog, withFileLock, getWorkflowPolicy, isProjectHookEnabled, ensureLocalGitRepo, getProactivityLevelForTask, getProactivityLevelForFindings, hasExplicitFindingSignal, shouldAutoCaptureFindingsForLevel, FINDING_SENSITIVITY_CONFIG, isFeatureEnabled, errorMessage, bootstrapPhrenDotEnv, finalizeTaskSession, appendFindingJournal, homePath, } from "./cli/hooks-context.js";
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
|
-
import {
|
|
9
|
+
import { spawnDetachedChild } from "./shared/process.js";
|
|
10
10
|
import { resolveSubprocessArgs as _resolveSubprocessArgs, runBestEffortGit, countUnsyncedCommits, recoverPushConflict, } from "./cli-hooks-git.js";
|
|
11
11
|
import { logger } from "./logger.js";
|
|
12
|
-
|
|
13
|
-
return resolveRuntimeProfile(getPhrenPath());
|
|
14
|
-
}
|
|
12
|
+
const SYNC_LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes
|
|
15
13
|
/** Read JSON from stdin if it's not a TTY. Returns null if stdin is a TTY or parsing fails. */
|
|
16
14
|
export function readStdinJson() {
|
|
17
15
|
if (process.stdin.isTTY)
|
|
@@ -112,7 +110,7 @@ function scheduleBackgroundSync(phrenPathLocal) {
|
|
|
112
110
|
try {
|
|
113
111
|
if (fs.existsSync(lockPath)) {
|
|
114
112
|
const ageMs = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
115
|
-
if (ageMs <=
|
|
113
|
+
if (ageMs <= SYNC_LOCK_STALE_MS)
|
|
116
114
|
return false;
|
|
117
115
|
fs.unlinkSync(lockPath);
|
|
118
116
|
}
|
|
@@ -125,16 +123,7 @@ function scheduleBackgroundSync(phrenPathLocal) {
|
|
|
125
123
|
fs.writeFileSync(lockPath, JSON.stringify({ startedAt: new Date().toISOString(), pid: process.pid }) + "\n", { flag: "wx" });
|
|
126
124
|
const logFd = fs.openSync(logPath, "a");
|
|
127
125
|
fs.writeSync(logFd, `[${new Date().toISOString()}] spawn ${process.execPath} ${spawnArgs.join(" ")}\n`);
|
|
128
|
-
const child =
|
|
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
|
-
});
|
|
126
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: phrenPathLocal, logFd });
|
|
138
127
|
child.unref();
|
|
139
128
|
fs.closeSync(logFd);
|
|
140
129
|
return true;
|
|
@@ -156,7 +145,7 @@ function scheduleWeeklyGovernance() {
|
|
|
156
145
|
if (daysSince >= 7) {
|
|
157
146
|
const spawnArgs = _resolveSubprocessArgs("background-maintenance");
|
|
158
147
|
if (spawnArgs) {
|
|
159
|
-
const child =
|
|
148
|
+
const child = spawnDetachedChild(spawnArgs, { phrenPath: getPhrenPath() });
|
|
160
149
|
child.unref();
|
|
161
150
|
fs.writeFileSync(lastGovPath, Date.now().toString());
|
|
162
151
|
debugLog("hook_stop: scheduled weekly governance run");
|
|
@@ -481,12 +470,7 @@ export async function handleBackgroundSync() {
|
|
|
481
470
|
const unsyncedCommits = await countUnsyncedCommits(phrenPathLocal);
|
|
482
471
|
updateRuntimeHealth(phrenPathLocal, {
|
|
483
472
|
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
|
-
},
|
|
473
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-local", pushDetail: "background sync skipped; no remote configured", unsyncedCommits }),
|
|
490
474
|
});
|
|
491
475
|
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-local detail=no_remote");
|
|
492
476
|
return;
|
|
@@ -495,12 +479,7 @@ export async function handleBackgroundSync() {
|
|
|
495
479
|
if (push.ok) {
|
|
496
480
|
updateRuntimeHealth(phrenPathLocal, {
|
|
497
481
|
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
|
-
},
|
|
482
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-pushed", pushDetail: "commit pushed by background sync", unsyncedCommits: 0 }),
|
|
504
483
|
});
|
|
505
484
|
appendAuditLog(phrenPathLocal, "background_sync", "status=saved-pushed");
|
|
506
485
|
return;
|
|
@@ -509,16 +488,7 @@ export async function handleBackgroundSync() {
|
|
|
509
488
|
if (recovered.ok) {
|
|
510
489
|
updateRuntimeHealth(phrenPathLocal, {
|
|
511
490
|
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
|
-
},
|
|
491
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-pushed", pushDetail: recovered.detail, pullAt: now, pullStatus: recovered.pullStatus, pullDetail: recovered.pullDetail, successfulPullAt: now, unsyncedCommits: 0 }),
|
|
522
492
|
});
|
|
523
493
|
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-pushed detail=${JSON.stringify(recovered.detail)}`);
|
|
524
494
|
return;
|
|
@@ -527,15 +497,7 @@ export async function handleBackgroundSync() {
|
|
|
527
497
|
const failDetail = recovered.detail || push.error || "background sync push failed";
|
|
528
498
|
updateRuntimeHealth(phrenPathLocal, {
|
|
529
499
|
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
|
-
},
|
|
500
|
+
lastSync: buildSyncStatus({ now, pushStatus: "saved-local", pushDetail: failDetail, pullAt: now, pullStatus: recovered.pullStatus, pullDetail: recovered.pullDetail, unsyncedCommits }),
|
|
539
501
|
});
|
|
540
502
|
appendAuditLog(phrenPathLocal, "background_sync", `status=saved-local detail=${JSON.stringify(failDetail)}`);
|
|
541
503
|
// Append to sync-warnings.jsonl so health_check and session_start can surface recent failures
|
|
@@ -4,6 +4,7 @@ import * as crypto from "crypto";
|
|
|
4
4
|
import { debugLog, runtimeFile, phrenOk, phrenErr, PhrenError, appendAuditLog, tryUnlink } from "../shared.js";
|
|
5
5
|
import { isValidProjectName, safeProjectPath, errorMessage } from "../utils.js";
|
|
6
6
|
import { withFileLock } from "../shared/governance.js";
|
|
7
|
+
import { walkDirectory } from "../shared/data-utils.js";
|
|
7
8
|
import { appendArchivedEntriesToTopicDoc, classifyTopicForText, readProjectTopics, topicReferencePath } from "../project-topics.js";
|
|
8
9
|
import { isCitationLine, isArchiveStart, isArchiveEnd, stripComments } from "./metadata.js";
|
|
9
10
|
import { logger } from "../logger.js";
|
|
@@ -81,28 +82,15 @@ function parseActiveEntries(content) {
|
|
|
81
82
|
/** Build a Set of normalized bullet strings from all .md files in referenceDir. */
|
|
82
83
|
function buildArchivedBulletSet(referenceDir) {
|
|
83
84
|
const bulletSet = new Set();
|
|
84
|
-
if (!fs.existsSync(referenceDir))
|
|
85
|
-
return bulletSet;
|
|
86
85
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const fullPath = path.join(current, entry.name);
|
|
92
|
-
if (entry.isDirectory()) {
|
|
93
|
-
stack.push(fullPath);
|
|
86
|
+
for (const filePath of walkDirectory(referenceDir)) {
|
|
87
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
88
|
+
for (const line of content.split("\n")) {
|
|
89
|
+
if (!line.startsWith("- "))
|
|
94
90
|
continue;
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
const content = fs.readFileSync(fullPath, "utf8");
|
|
99
|
-
for (const line of content.split("\n")) {
|
|
100
|
-
if (!line.startsWith("- "))
|
|
101
|
-
continue;
|
|
102
|
-
const normalizedLine = stripComments(line).replace(/^-\s+/, "").trim().toLowerCase();
|
|
103
|
-
if (normalizedLine)
|
|
104
|
-
bulletSet.add(normalizedLine);
|
|
105
|
-
}
|
|
91
|
+
const normalizedLine = stripComments(line).replace(/^-\s+/, "").trim().toLowerCase();
|
|
92
|
+
if (normalizedLine)
|
|
93
|
+
bulletSet.add(normalizedLine);
|
|
106
94
|
}
|
|
107
95
|
}
|
|
108
96
|
}
|