@phren/cli 0.0.36 → 0.0.37
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-hooks-stop.js +28 -0
- package/mcp/dist/content/learning.js +2 -2
- package/mcp/dist/governance/locks.js +5 -34
- package/mcp/dist/governance/policy.js +2 -2
- package/mcp/dist/init/setup.js +15 -5
- package/mcp/dist/init-uninstall.js +11 -2
- package/mcp/dist/phren-paths.js +10 -1
- package/mcp/dist/shared/index.js +8 -0
- package/mcp/dist/task/lifecycle.js +1 -1
- package/package.json +1 -1
|
@@ -434,6 +434,34 @@ export async function handleHookStop() {
|
|
|
434
434
|
}
|
|
435
435
|
return;
|
|
436
436
|
}
|
|
437
|
+
// Check if HEAD has an upstream tracking branch before attempting sync.
|
|
438
|
+
// Detached HEAD or branches without upstream would cause silent push failures.
|
|
439
|
+
const upstream = await runBestEffortGit(["rev-parse", "--abbrev-ref", "@{upstream}"], phrenPath);
|
|
440
|
+
if (!upstream.ok || !upstream.output) {
|
|
441
|
+
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
442
|
+
const noUpstreamDetail = "commit created; no upstream tracking branch";
|
|
443
|
+
finalizeTaskSession({
|
|
444
|
+
phrenPath,
|
|
445
|
+
sessionId: taskSessionId,
|
|
446
|
+
status: "no-upstream",
|
|
447
|
+
detail: noUpstreamDetail,
|
|
448
|
+
});
|
|
449
|
+
updateRuntimeHealth(phrenPath, {
|
|
450
|
+
lastStopAt: now,
|
|
451
|
+
lastAutoSave: { at: now, status: "no-upstream", detail: noUpstreamDetail },
|
|
452
|
+
lastSync: {
|
|
453
|
+
lastPushAt: now,
|
|
454
|
+
lastPushStatus: "no-upstream",
|
|
455
|
+
lastPushDetail: noUpstreamDetail,
|
|
456
|
+
unsyncedCommits,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
appendAuditLog(phrenPath, "hook_stop", "status=no-upstream");
|
|
460
|
+
if (unsyncedCommits > 3) {
|
|
461
|
+
process.stderr.write(`phren: ${unsyncedCommits} unsynced commits — no upstream tracking branch.\n`);
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
437
465
|
const unsyncedCommits = await countUnsyncedCommits(phrenPath);
|
|
438
466
|
const scheduled = scheduleBackgroundSync(phrenPath);
|
|
439
467
|
const syncDetail = scheduled
|
|
@@ -305,7 +305,7 @@ export function addFindingToFile(phrenPath, project, learning, citationInput, op
|
|
|
305
305
|
return phrenOk(`Skipped duplicate finding for "${project}": already exists with similar wording.`);
|
|
306
306
|
}
|
|
307
307
|
const newContent = `# ${project} Findings\n\n## ${today}\n\n${preparedForNewFile.finding.bullet}\n${preparedForNewFile.finding.citationComment}\n`;
|
|
308
|
-
const tmpPath = learningsPath +
|
|
308
|
+
const tmpPath = learningsPath + `.tmp-${crypto.randomUUID()}`;
|
|
309
309
|
fs.writeFileSync(tmpPath, newContent);
|
|
310
310
|
fs.renameSync(tmpPath, learningsPath);
|
|
311
311
|
return phrenOk({
|
|
@@ -461,7 +461,7 @@ export function addFindingsToFile(phrenPath, project, learnings, opts) {
|
|
|
461
461
|
added.push(learning);
|
|
462
462
|
}
|
|
463
463
|
if (added.length > 0) {
|
|
464
|
-
const tmpPath = learningsPath +
|
|
464
|
+
const tmpPath = learningsPath + `.tmp-${crypto.randomUUID()}`;
|
|
465
465
|
fs.writeFileSync(tmpPath, content.endsWith("\n") ? content : `${content}\n`);
|
|
466
466
|
fs.renameSync(tmpPath, learningsPath);
|
|
467
467
|
}
|
|
@@ -24,40 +24,11 @@ function acquireFileLock(lockPath) {
|
|
|
24
24
|
try {
|
|
25
25
|
const stat = fs.statSync(lockPath);
|
|
26
26
|
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (Number.isFinite(lockPid) && lockPid > 0) {
|
|
33
|
-
if (process.platform !== 'win32') {
|
|
34
|
-
try {
|
|
35
|
-
process.kill(lockPid, 0);
|
|
36
|
-
ownerDead = false;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
ownerDead = true;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
try {
|
|
44
|
-
const result = require('child_process').spawnSync('tasklist', ['/FI', `PID eq ${lockPid}`, '/NH'], { encoding: 'utf8', timeout: 2000 });
|
|
45
|
-
if (result.stdout && result.stdout.includes(String(lockPid)))
|
|
46
|
-
ownerDead = false;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
ownerDead = true;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
ownerDead = true; // Can't read lock file, treat as dead
|
|
56
|
-
}
|
|
57
|
-
if (ownerDead) {
|
|
58
|
-
fs.unlinkSync(lockPath);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
27
|
+
// Lock file is older than stale threshold — delete unconditionally.
|
|
28
|
+
// This handles zombie processes, crashed hooks, and any case where
|
|
29
|
+
// the owning process failed to clean up.
|
|
30
|
+
fs.unlinkSync(lockPath);
|
|
31
|
+
continue;
|
|
61
32
|
}
|
|
62
33
|
}
|
|
63
34
|
catch (statErr) {
|
|
@@ -143,7 +143,7 @@ function normalizeRuntimeHealth(data) {
|
|
|
143
143
|
normalized.lastPromptAt = data.lastPromptAt;
|
|
144
144
|
if (typeof data.lastStopAt === "string")
|
|
145
145
|
normalized.lastStopAt = data.lastStopAt;
|
|
146
|
-
if (isRecord(data.lastAutoSave) && typeof data.lastAutoSave.at === "string" && ["clean", "saved-local", "saved-pushed", "error"].includes(String(data.lastAutoSave.status))) {
|
|
146
|
+
if (isRecord(data.lastAutoSave) && typeof data.lastAutoSave.at === "string" && ["clean", "saved-local", "saved-pushed", "no-upstream", "error"].includes(String(data.lastAutoSave.status))) {
|
|
147
147
|
normalized.lastAutoSave = {
|
|
148
148
|
at: data.lastAutoSave.at,
|
|
149
149
|
status: data.lastAutoSave.status,
|
|
@@ -169,7 +169,7 @@ function normalizeRuntimeHealth(data) {
|
|
|
169
169
|
normalized.lastSync.lastSuccessfulPullAt = data.lastSync.lastSuccessfulPullAt;
|
|
170
170
|
if (typeof data.lastSync.lastPushAt === "string")
|
|
171
171
|
normalized.lastSync.lastPushAt = data.lastSync.lastPushAt;
|
|
172
|
-
if (["saved-local", "saved-pushed", "error"].includes(String(data.lastSync.lastPushStatus)))
|
|
172
|
+
if (["saved-local", "saved-pushed", "no-upstream", "error"].includes(String(data.lastSync.lastPushStatus)))
|
|
173
173
|
normalized.lastSync.lastPushStatus = data.lastSync.lastPushStatus;
|
|
174
174
|
if (typeof data.lastSync.lastPushDetail === "string")
|
|
175
175
|
normalized.lastSync.lastPushDetail = data.lastSync.lastPushDetail;
|
package/mcp/dist/init/setup.js
CHANGED
|
@@ -884,15 +884,25 @@ export function ensureProjectScaffold(projectDir, projectName, domain = "softwar
|
|
|
884
884
|
}
|
|
885
885
|
}
|
|
886
886
|
export function ensureLocalGitRepo(phrenPath) {
|
|
887
|
+
// Check if phrenPath already has its own git repo (not just being inside a parent)
|
|
887
888
|
try {
|
|
888
|
-
execFileSync("git", ["-C", phrenPath, "rev-parse", "--
|
|
889
|
-
|
|
889
|
+
const topLevel = execFileSync("git", ["-C", phrenPath, "rev-parse", "--show-toplevel"], {
|
|
890
|
+
encoding: "utf8",
|
|
891
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
890
892
|
timeout: EXEC_TIMEOUT_QUICK_MS,
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
+
}).trim();
|
|
894
|
+
const resolvedTopLevel = path.resolve(topLevel);
|
|
895
|
+
const resolvedPhrenPath = path.resolve(phrenPath);
|
|
896
|
+
if (resolvedTopLevel === resolvedPhrenPath) {
|
|
897
|
+
// phrenPath IS the repo root — it has its own git repo
|
|
898
|
+
return { ok: true, initialized: false, detail: "existing git repo" };
|
|
899
|
+
}
|
|
900
|
+
// phrenPath is inside a parent repo — skip nested init
|
|
901
|
+
logger.warn("init", `Skipping git init: ${resolvedPhrenPath} is inside existing repo ${resolvedTopLevel}`);
|
|
902
|
+
return { ok: true, initialized: false, detail: `skipped: inside existing repo ${resolvedTopLevel}` };
|
|
893
903
|
}
|
|
894
904
|
catch {
|
|
895
|
-
//
|
|
905
|
+
// Not inside any git repo — fall through to initialization below.
|
|
896
906
|
}
|
|
897
907
|
try {
|
|
898
908
|
try {
|
|
@@ -170,8 +170,17 @@ function filterAgentHooks(filePath, commandField) {
|
|
|
170
170
|
return true;
|
|
171
171
|
}
|
|
172
172
|
catch (err) {
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
// JSON parse or other failure — back up the corrupted file so uninstall can proceed
|
|
174
|
+
const bakPath = filePath + ".bak";
|
|
175
|
+
try {
|
|
176
|
+
fs.renameSync(filePath, bakPath);
|
|
177
|
+
log(` Warning: corrupted hook config backed up to ${bakPath} (${errorMessage(err)})`);
|
|
178
|
+
}
|
|
179
|
+
catch (bakErr) {
|
|
180
|
+
debugLog(`filterAgentHooks: backup failed for ${filePath}: ${errorMessage(bakErr)}`);
|
|
181
|
+
log(` Warning: could not process hook config ${filePath}: ${errorMessage(err)}`);
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
async function promptUninstallConfirm(phrenPath) {
|
package/mcp/dist/phren-paths.js
CHANGED
|
@@ -35,7 +35,16 @@ export function atomicWriteText(filePath, content) {
|
|
|
35
35
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
36
|
const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
|
|
37
37
|
fs.writeFileSync(tmpPath, content);
|
|
38
|
-
|
|
38
|
+
try {
|
|
39
|
+
fs.renameSync(tmpPath, filePath);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(tmpPath);
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
39
48
|
}
|
|
40
49
|
function isInstallMode(value) {
|
|
41
50
|
return value === "shared" || value === "project-local";
|
package/mcp/dist/shared/index.js
CHANGED
|
@@ -22,7 +22,13 @@ export { buildSourceDocKey, decodeFiniteNumber, decodeStringRow, extractSnippet,
|
|
|
22
22
|
// ── Async embedding queue ───────────────────────────────────────────────────
|
|
23
23
|
const _embQueue = new Map();
|
|
24
24
|
let _embTimer = null;
|
|
25
|
+
const MAX_EMB_QUEUE = 500;
|
|
25
26
|
function scheduleEmbedding(phrenPath, docPath, content) {
|
|
27
|
+
if (_embQueue.size >= MAX_EMB_QUEUE) {
|
|
28
|
+
const oldest = _embQueue.keys().next().value;
|
|
29
|
+
if (oldest !== undefined)
|
|
30
|
+
_embQueue.delete(oldest);
|
|
31
|
+
}
|
|
26
32
|
_embQueue.set(docPath, { phrenPath, content });
|
|
27
33
|
if (_embTimer)
|
|
28
34
|
clearTimeout(_embTimer);
|
|
@@ -63,6 +69,7 @@ async function _drainEmbQueue() {
|
|
|
63
69
|
}
|
|
64
70
|
catch (err) {
|
|
65
71
|
logger.debug("embeddingQueue embedText", errorMessage(err));
|
|
72
|
+
_embQueue.clear();
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
try {
|
|
@@ -70,6 +77,7 @@ async function _drainEmbQueue() {
|
|
|
70
77
|
}
|
|
71
78
|
catch (err) {
|
|
72
79
|
logger.debug("embeddingQueue cacheFlush", errorMessage(err));
|
|
80
|
+
_embQueue.clear();
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
}
|
|
@@ -302,7 +302,7 @@ export function finalizeTaskSession(args) {
|
|
|
302
302
|
if (!state || state.mode !== "auto")
|
|
303
303
|
return;
|
|
304
304
|
const match = state.stableId ? `bid:${state.stableId}` : state.item;
|
|
305
|
-
if (args.status === "saved-local" || args.status === "saved-pushed") {
|
|
305
|
+
if (args.status === "saved-local" || args.status === "saved-pushed" || args.status === "no-upstream") {
|
|
306
306
|
const completed = completeTask(args.phrenPath, state.project, match);
|
|
307
307
|
if (!completed.ok) {
|
|
308
308
|
debugLog(`task lifecycle complete ${state.project}: ${completed.error}`);
|