@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/embedding.js
CHANGED
|
@@ -79,7 +79,7 @@ async function openCacheDb(phrenPath) {
|
|
|
79
79
|
db?.close();
|
|
80
80
|
}
|
|
81
81
|
catch (e2) {
|
|
82
|
-
if ((process.env.PHREN_DEBUG
|
|
82
|
+
if ((process.env.PHREN_DEBUG))
|
|
83
83
|
process.stderr.write(`[phren] embedding openCacheDb dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
84
84
|
}
|
|
85
85
|
throw err;
|
|
@@ -126,13 +126,13 @@ function persistDb(phrenPath, db) {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
catch (err) {
|
|
129
|
-
if ((process.env.PHREN_DEBUG
|
|
130
|
-
process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${
|
|
129
|
+
if ((process.env.PHREN_DEBUG))
|
|
130
|
+
process.stderr.write(`[phren] embedding persistDb onDiskLoad: ${errorMessage(err)}\n`);
|
|
131
131
|
try {
|
|
132
132
|
onDisk?.close();
|
|
133
133
|
}
|
|
134
134
|
catch (e2) {
|
|
135
|
-
if ((process.env.PHREN_DEBUG
|
|
135
|
+
if ((process.env.PHREN_DEBUG))
|
|
136
136
|
process.stderr.write(`[phren] embedding persistDb onDiskClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
137
137
|
}
|
|
138
138
|
onDisk = null;
|
|
@@ -149,7 +149,7 @@ function persistDb(phrenPath, db) {
|
|
|
149
149
|
onDisk.close();
|
|
150
150
|
}
|
|
151
151
|
catch (e2) {
|
|
152
|
-
if ((process.env.PHREN_DEBUG
|
|
152
|
+
if ((process.env.PHREN_DEBUG))
|
|
153
153
|
process.stderr.write(`[phren] embedding persistDb onDiskCloseFinally: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -269,7 +269,7 @@ export async function getCachedEmbedding(phrenPath, text, apiKey, model) {
|
|
|
269
269
|
db?.close();
|
|
270
270
|
}
|
|
271
271
|
catch (e2) {
|
|
272
|
-
if ((process.env.PHREN_DEBUG
|
|
272
|
+
if ((process.env.PHREN_DEBUG))
|
|
273
273
|
process.stderr.write(`[phren] embedding getCachedEmbedding dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
274
274
|
}
|
|
275
275
|
}
|
|
@@ -320,7 +320,7 @@ export async function getCachedEmbeddings(phrenPath, texts, apiKey, model) {
|
|
|
320
320
|
db?.close();
|
|
321
321
|
}
|
|
322
322
|
catch (e2) {
|
|
323
|
-
if ((process.env.PHREN_DEBUG
|
|
323
|
+
if ((process.env.PHREN_DEBUG))
|
|
324
324
|
process.stderr.write(`[phren] embedding getCachedEmbeddings dbClose: ${e2 instanceof Error ? e2.message : String(e2)}\n`);
|
|
325
325
|
}
|
|
326
326
|
}
|
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -5,110 +5,124 @@ import { errorMessage } from "./utils.js";
|
|
|
5
5
|
import { defaultPhrenPath, findPhrenPath } from "./shared.js";
|
|
6
6
|
import { addProjectFromPath } from "./core-project.js";
|
|
7
7
|
import { PROJECT_OWNERSHIP_MODES, getProjectOwnershipDefault, parseProjectOwnershipMode, } from "./project-config.js";
|
|
8
|
-
const HELP_TEXT = `phren -
|
|
8
|
+
const HELP_TEXT = `phren - persistent knowledge for your agents
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
phren
|
|
12
|
-
phren
|
|
13
|
-
phren
|
|
14
|
-
phren
|
|
15
|
-
|
|
16
|
-
phren
|
|
10
|
+
phren Interactive shell
|
|
11
|
+
phren init Set up phren
|
|
12
|
+
phren add [path] Register a project
|
|
13
|
+
phren search <query> Search what phren knows
|
|
14
|
+
phren status Health check
|
|
15
|
+
phren doctor [--fix] Diagnose and repair
|
|
16
|
+
phren web-ui Open the knowledge graph
|
|
17
|
+
phren tasks Cross-project task view
|
|
18
|
+
phren graph Fragment knowledge graph
|
|
19
|
+
|
|
20
|
+
phren help <topic> Detailed help for a topic
|
|
21
|
+
|
|
22
|
+
Topics: projects, skills, hooks, config, maintain, setup, env, all
|
|
23
|
+
`;
|
|
24
|
+
const HELP_TOPICS = {
|
|
25
|
+
projects: `Projects:
|
|
26
|
+
phren add [path] [--ownership <mode>] Register a project
|
|
27
|
+
phren projects list List all tracked projects
|
|
17
28
|
phren projects configure <name> [--ownership <mode>] [--hooks on|off]
|
|
18
|
-
|
|
19
|
-
phren projects remove <name>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
phren skills
|
|
23
|
-
phren skills
|
|
24
|
-
phren skills
|
|
25
|
-
phren skills
|
|
26
|
-
phren skills
|
|
27
|
-
phren
|
|
28
|
-
phren
|
|
29
|
-
phren
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
phren
|
|
33
|
-
phren
|
|
34
|
-
phren
|
|
35
|
-
|
|
36
|
-
phren
|
|
37
|
-
phren
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
phren
|
|
41
|
-
|
|
42
|
-
phren
|
|
43
|
-
phren
|
|
44
|
-
phren
|
|
45
|
-
phren
|
|
46
|
-
phren
|
|
47
|
-
|
|
48
|
-
phren
|
|
49
|
-
|
|
29
|
+
Update per-project settings
|
|
30
|
+
phren projects remove <name> Remove a project
|
|
31
|
+
`,
|
|
32
|
+
skills: `Skills:
|
|
33
|
+
phren skills list List installed skills
|
|
34
|
+
phren skills add <project> <path> Link a skill into a project
|
|
35
|
+
phren skills show <name> Show skill content
|
|
36
|
+
phren skills resolve <project|global> Print resolved skill manifest
|
|
37
|
+
phren skills doctor <project|global> Diagnose skill visibility
|
|
38
|
+
phren skills sync <project|global> Regenerate skill mirror
|
|
39
|
+
phren skills remove <project> <name> Remove a skill
|
|
40
|
+
phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
|
|
41
|
+
`,
|
|
42
|
+
hooks: `Hooks:
|
|
43
|
+
phren hooks list [--project <name>] Show hook status per tool
|
|
44
|
+
phren hooks enable <tool> Enable hooks for a tool
|
|
45
|
+
phren hooks disable <tool> Disable hooks for a tool
|
|
46
|
+
phren hooks add-custom <event> <cmd> Add a custom hook
|
|
47
|
+
phren hooks remove-custom <event> Remove custom hooks
|
|
48
|
+
phren hooks errors [--limit <n>] Show recent hook errors
|
|
49
|
+
`,
|
|
50
|
+
config: `Configuration:
|
|
51
|
+
phren config show [--project <name>] Show current config
|
|
52
|
+
phren config policy [get|set ...] Retention, TTL, confidence, decay
|
|
53
|
+
phren config workflow [get|set ...] Risky-memory thresholds
|
|
54
|
+
phren config proactivity [level] Set proactivity level
|
|
55
|
+
phren config task-mode [mode] Set task automation mode
|
|
56
|
+
phren config finding-sensitivity [lvl] Set finding capture sensitivity
|
|
57
|
+
phren config index [get|set ...] Indexer include/exclude globs
|
|
58
|
+
phren config synonyms [list|add|remove] Manage learned synonyms
|
|
59
|
+
phren config project-ownership [mode] Default ownership for new projects
|
|
60
|
+
phren config machines Registered machines
|
|
61
|
+
phren config profiles Profiles and projects
|
|
62
|
+
phren config telemetry [on|off] Opt-in usage telemetry
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
phren
|
|
55
|
-
phren
|
|
56
|
-
|
|
57
|
-
phren
|
|
58
|
-
|
|
59
|
-
|
|
64
|
+
All config subcommands accept --project <name> for per-project overrides.
|
|
65
|
+
`,
|
|
66
|
+
maintain: `Maintenance:
|
|
67
|
+
phren maintain govern [project] Queue stale memories for review
|
|
68
|
+
phren maintain prune [project] Delete expired entries
|
|
69
|
+
phren maintain consolidate [project] Deduplicate findings
|
|
70
|
+
phren maintain extract [project] Mine git/GitHub signals
|
|
71
|
+
`,
|
|
72
|
+
setup: `Setup:
|
|
73
|
+
phren init [--mode shared|project-local] [--machine <n>] [--profile <n>] [--dry-run] [-y]
|
|
74
|
+
phren quickstart Quick setup: init + project scaffold
|
|
75
|
+
phren mcp-mode [on|off|status] Toggle MCP integration
|
|
76
|
+
phren hooks-mode [on|off|status] Toggle hook execution
|
|
77
|
+
phren verify Check init completed OK
|
|
78
|
+
phren uninstall Remove phren config and hooks
|
|
79
|
+
phren update [--refresh-starter] Update to latest version
|
|
80
|
+
`,
|
|
81
|
+
env: `Environment variables:
|
|
82
|
+
PHREN_PATH Override phren directory (default: ~/.phren)
|
|
83
|
+
PHREN_PROFILE Active profile name
|
|
84
|
+
PHREN_DEBUG Enable debug logging (set to 1)
|
|
60
85
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
Embeddings:
|
|
87
|
+
PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434, 'off' to disable)
|
|
88
|
+
PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint
|
|
89
|
+
PHREN_EMBEDDING_API_KEY API key for embedding endpoint
|
|
90
|
+
PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
|
|
66
91
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
phren uninstall Remove phren config and hooks
|
|
92
|
+
Context injection:
|
|
93
|
+
PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per prompt (default: 550)
|
|
94
|
+
PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
|
|
95
|
+
PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
|
|
72
96
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint (cloud alternative to Ollama)
|
|
79
|
-
PHREN_EMBEDDING_API_KEY API key for PHREN_EMBEDDING_API_URL
|
|
80
|
-
PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
|
|
81
|
-
PHREN_EXTRACT_MODEL Ollama model for memory extraction (default: llama3.2)
|
|
82
|
-
PHREN_EMBEDDING_PROVIDER Set to 'api' to use OpenAI API for search_knowledge embeddings
|
|
83
|
-
PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations at session end
|
|
84
|
-
PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
|
|
85
|
-
PHREN_FEATURE_SEMANTIC_CONFLICT=1 LLM-based conflict detection on add_finding
|
|
86
|
-
PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback in search_knowledge
|
|
87
|
-
PHREN_FEATURE_AUTO_EXTRACT=0 Disable automatic memory extraction on each prompt
|
|
88
|
-
PHREN_FEATURE_PROGRESSIVE_DISCLOSURE=1 Compact memory index injection
|
|
89
|
-
PHREN_LLM_MODEL LLM model for semantic dedup/conflict (default: gpt-4o-mini)
|
|
90
|
-
PHREN_LLM_ENDPOINT OpenAI-compatible /chat/completions base URL for dedup/conflict
|
|
91
|
-
PHREN_LLM_KEY API key for PHREN_LLM_ENDPOINT
|
|
92
|
-
PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per hook-prompt (default: 550)
|
|
93
|
-
PHREN_CONTEXT_SNIPPET_LINES Max lines per injected snippet (default: 6)
|
|
94
|
-
PHREN_CONTEXT_SNIPPET_CHARS Max chars per injected snippet (default: 520)
|
|
95
|
-
PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
|
|
96
|
-
PHREN_TASK_PRIORITY Priorities to include in task injection: high,medium,low (default: high,medium)
|
|
97
|
-
PHREN_MEMORY_TTL_DAYS Override memory TTL for trust filtering
|
|
98
|
-
PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
|
|
99
|
-
PHREN_FINDINGS_CAP Max findings per date section before consolidation (default: 20)
|
|
100
|
-
PHREN_GH_PR_LIMIT/RUN_LIMIT/ISSUE_LIMIT GitHub extraction limits (defaults: 40/25/25)
|
|
97
|
+
Feature flags:
|
|
98
|
+
PHREN_FEATURE_AUTO_EXTRACT=0 Disable auto memory extraction
|
|
99
|
+
PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations
|
|
100
|
+
PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
|
|
101
|
+
PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
phren
|
|
110
|
-
phren
|
|
111
|
-
|
|
103
|
+
Run 'phren help all' to see everything.
|
|
104
|
+
`,
|
|
105
|
+
};
|
|
106
|
+
function buildFullHelp() {
|
|
107
|
+
return `phren - persistent knowledge for your agents
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
phren Interactive shell
|
|
111
|
+
phren init Set up phren
|
|
112
|
+
phren add [path] Register a project
|
|
113
|
+
phren search <query> Search what phren knows
|
|
114
|
+
phren status Health check
|
|
115
|
+
phren doctor [--fix] Diagnose and repair
|
|
116
|
+
phren web-ui Open the knowledge graph
|
|
117
|
+
phren tasks Cross-project task view
|
|
118
|
+
phren graph Fragment knowledge graph
|
|
119
|
+
phren add-finding <p> "." Tell phren what you learned
|
|
120
|
+
phren pin <p> "..." Save a truth
|
|
121
|
+
phren review [project] Show review queue
|
|
122
|
+
phren session-context Current session state
|
|
123
|
+
|
|
124
|
+
${Object.values(HELP_TOPICS).join("\n")}`;
|
|
125
|
+
}
|
|
112
126
|
const CLI_COMMANDS = [
|
|
113
127
|
"search",
|
|
114
128
|
"shell",
|
|
@@ -216,7 +230,19 @@ async function promptProjectOwnership(phrenPath, fallback) {
|
|
|
216
230
|
export async function runTopLevelCommand(argv) {
|
|
217
231
|
const argvCommand = argv[0];
|
|
218
232
|
if (argvCommand === "--help" || argvCommand === "-h" || argvCommand === "help") {
|
|
219
|
-
|
|
233
|
+
const topic = argv[1]?.toLowerCase();
|
|
234
|
+
if (topic === "all") {
|
|
235
|
+
console.log(buildFullHelp());
|
|
236
|
+
}
|
|
237
|
+
else if (topic && HELP_TOPICS[topic]) {
|
|
238
|
+
console.log(HELP_TOPICS[topic]);
|
|
239
|
+
}
|
|
240
|
+
else if (topic) {
|
|
241
|
+
console.log(`Unknown topic: ${topic}\nAvailable: ${Object.keys(HELP_TOPICS).join(", ")}, all`);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(HELP_TEXT);
|
|
245
|
+
}
|
|
220
246
|
return finish();
|
|
221
247
|
}
|
|
222
248
|
if (argvCommand === "add") {
|
|
@@ -313,7 +339,8 @@ export async function runTopLevelCommand(argv) {
|
|
|
313
339
|
}
|
|
314
340
|
if (argvCommand === "uninstall") {
|
|
315
341
|
const { runUninstall } = await import("./init.js");
|
|
316
|
-
|
|
342
|
+
const skipConfirm = argv.includes("--yes") || argv.includes("-y");
|
|
343
|
+
await runUninstall({ yes: skipConfirm });
|
|
317
344
|
return finish();
|
|
318
345
|
}
|
|
319
346
|
if (argvCommand === "status") {
|
|
@@ -349,7 +376,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
349
376
|
return finish();
|
|
350
377
|
}
|
|
351
378
|
catch (err) {
|
|
352
|
-
console.error(
|
|
379
|
+
console.error(errorMessage(err));
|
|
353
380
|
return finish(1);
|
|
354
381
|
}
|
|
355
382
|
}
|
|
@@ -360,7 +387,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
360
387
|
return finish();
|
|
361
388
|
}
|
|
362
389
|
catch (err) {
|
|
363
|
-
console.error(
|
|
390
|
+
console.error(errorMessage(err));
|
|
364
391
|
return finish(1);
|
|
365
392
|
}
|
|
366
393
|
}
|
|
@@ -383,7 +410,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
383
410
|
trackCliCommand(defaultPhrenPath(), argvCommand);
|
|
384
411
|
}
|
|
385
412
|
catch (err) {
|
|
386
|
-
if ((process.env.PHREN_DEBUG
|
|
413
|
+
if ((process.env.PHREN_DEBUG))
|
|
387
414
|
process.stderr.write(`[phren] cli trackCliCommand: ${errorMessage(err)}\n`);
|
|
388
415
|
}
|
|
389
416
|
await runCliCommand(argvCommand, argv.slice(1));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { debugLog } from "./shared.js";
|
|
4
|
+
import { errorMessage } from "./utils.js";
|
|
4
5
|
// Acquire the file lock, returning true on success or throwing on timeout.
|
|
5
6
|
function acquireFileLock(lockPath) {
|
|
6
7
|
const maxWait = Number.parseInt(process.env.PHREN_FILE_LOCK_MAX_WAIT_MS || "5000", 10) || 5000;
|
|
@@ -18,8 +19,8 @@ function acquireFileLock(lockPath) {
|
|
|
18
19
|
break;
|
|
19
20
|
}
|
|
20
21
|
catch (err) {
|
|
21
|
-
if ((process.env.PHREN_DEBUG
|
|
22
|
-
process.stderr.write(`[phren] acquireFileLock lockWrite: ${
|
|
22
|
+
if ((process.env.PHREN_DEBUG))
|
|
23
|
+
process.stderr.write(`[phren] acquireFileLock lockWrite: ${errorMessage(err)}\n`);
|
|
23
24
|
try {
|
|
24
25
|
const stat = fs.statSync(lockPath);
|
|
25
26
|
if (Date.now() - stat.mtimeMs > staleThreshold) {
|
|
@@ -53,7 +54,7 @@ function acquireFileLock(lockPath) {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
catch (statErr) {
|
|
56
|
-
if ((process.env.PHREN_DEBUG
|
|
57
|
+
if ((process.env.PHREN_DEBUG))
|
|
57
58
|
process.stderr.write(`[phren] acquireFileLock staleStat: ${statErr instanceof Error ? statErr.message : String(statErr)}\n`);
|
|
58
59
|
sleep(pollInterval);
|
|
59
60
|
waited += pollInterval;
|
|
@@ -74,8 +75,8 @@ function releaseFileLock(lockPath) {
|
|
|
74
75
|
fs.unlinkSync(lockPath);
|
|
75
76
|
}
|
|
76
77
|
catch (err) {
|
|
77
|
-
if ((process.env.PHREN_DEBUG
|
|
78
|
-
process.stderr.write(`[phren] releaseFileLock: ${
|
|
78
|
+
if ((process.env.PHREN_DEBUG))
|
|
79
|
+
process.stderr.write(`[phren] releaseFileLock: ${errorMessage(err)}\n`);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
// Q10: withFileLock now accepts both sync and async callbacks.
|
|
@@ -4,6 +4,7 @@ import * as path from "path";
|
|
|
4
4
|
import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "./shared.js";
|
|
5
5
|
import { withFileLock } from "./governance-locks.js";
|
|
6
6
|
import { errorMessage, isValidProjectName, safeProjectPath } from "./utils.js";
|
|
7
|
+
import { readProjectConfig } from "./project-config.js";
|
|
7
8
|
import { runCustomHooks } from "./hooks.js";
|
|
8
9
|
import { METADATA_REGEX, isCitationLine, isArchiveStart as isArchiveStartMeta, isArchiveEnd as isArchiveEndMeta, stripLifecycleMetadata as stripLifecycleMetadataMeta, } from "./content-metadata.js";
|
|
9
10
|
export const MAX_QUEUE_ENTRY_LENGTH = 500;
|
|
@@ -212,6 +213,148 @@ function normalizeIndexPolicy(data) {
|
|
|
212
213
|
includeHidden: pickBoolean(data.includeHidden, DEFAULT_INDEX_POLICY.includeHidden),
|
|
213
214
|
};
|
|
214
215
|
}
|
|
216
|
+
const VALID_PROACTIVITY_LEVELS = ["high", "medium", "low"];
|
|
217
|
+
export const VALID_TASK_MODES = ["off", "manual", "suggest", "auto"];
|
|
218
|
+
export const VALID_FINDING_SENSITIVITY = ["minimal", "conservative", "balanced", "aggressive"];
|
|
219
|
+
const VALID_RISKY_SECTIONS = ["Review", "Stale", "Conflicts"];
|
|
220
|
+
function pickEnum(value, allowed) {
|
|
221
|
+
return typeof value === "string" && allowed.includes(value) ? value : undefined;
|
|
222
|
+
}
|
|
223
|
+
function pickPositiveInt(value) {
|
|
224
|
+
return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
|
|
225
|
+
}
|
|
226
|
+
function pickUnitInterval(value) {
|
|
227
|
+
return isFiniteNumber(value) && value >= 0 && value <= 1 ? value : undefined;
|
|
228
|
+
}
|
|
229
|
+
function normalizeProjectConfigOverrides(raw) {
|
|
230
|
+
if (!isRecord(raw))
|
|
231
|
+
return undefined;
|
|
232
|
+
const retentionRaw = isRecord(raw.retentionPolicy) ? raw.retentionPolicy : undefined;
|
|
233
|
+
const decayRaw = retentionRaw && isRecord(retentionRaw.decay) ? retentionRaw.decay : undefined;
|
|
234
|
+
const retentionPolicy = retentionRaw
|
|
235
|
+
? {
|
|
236
|
+
ttlDays: pickPositiveInt(retentionRaw.ttlDays),
|
|
237
|
+
retentionDays: pickPositiveInt(retentionRaw.retentionDays),
|
|
238
|
+
autoAcceptThreshold: pickUnitInterval(retentionRaw.autoAcceptThreshold),
|
|
239
|
+
minInjectConfidence: pickUnitInterval(retentionRaw.minInjectConfidence),
|
|
240
|
+
decay: decayRaw
|
|
241
|
+
? {
|
|
242
|
+
d30: pickUnitInterval(decayRaw.d30),
|
|
243
|
+
d60: pickUnitInterval(decayRaw.d60),
|
|
244
|
+
d90: pickUnitInterval(decayRaw.d90),
|
|
245
|
+
d120: pickUnitInterval(decayRaw.d120),
|
|
246
|
+
}
|
|
247
|
+
: undefined,
|
|
248
|
+
}
|
|
249
|
+
: undefined;
|
|
250
|
+
if (retentionPolicy && retentionPolicy.decay && Object.values(retentionPolicy.decay).every((value) => value === undefined)) {
|
|
251
|
+
delete retentionPolicy.decay;
|
|
252
|
+
}
|
|
253
|
+
const workflowRaw = isRecord(raw.workflowPolicy) ? raw.workflowPolicy : undefined;
|
|
254
|
+
const workflowPolicy = workflowRaw
|
|
255
|
+
? {
|
|
256
|
+
lowConfidenceThreshold: pickUnitInterval(workflowRaw.lowConfidenceThreshold),
|
|
257
|
+
riskySections: Array.isArray(workflowRaw.riskySections)
|
|
258
|
+
? workflowRaw.riskySections.filter((section) => typeof section === "string" && VALID_RISKY_SECTIONS.includes(section))
|
|
259
|
+
: undefined,
|
|
260
|
+
}
|
|
261
|
+
: undefined;
|
|
262
|
+
if (workflowPolicy && workflowPolicy.riskySections && workflowPolicy.riskySections.length === 0) {
|
|
263
|
+
delete workflowPolicy.riskySections;
|
|
264
|
+
}
|
|
265
|
+
const overrides = {
|
|
266
|
+
findingSensitivity: pickEnum(raw.findingSensitivity, VALID_FINDING_SENSITIVITY),
|
|
267
|
+
proactivity: pickEnum(raw.proactivity, VALID_PROACTIVITY_LEVELS),
|
|
268
|
+
proactivityFindings: pickEnum(raw.proactivityFindings, VALID_PROACTIVITY_LEVELS),
|
|
269
|
+
proactivityTask: pickEnum(raw.proactivityTask, VALID_PROACTIVITY_LEVELS),
|
|
270
|
+
taskMode: pickEnum(raw.taskMode, VALID_TASK_MODES),
|
|
271
|
+
retentionPolicy: retentionPolicy && Object.values(retentionPolicy).some((value) => value !== undefined)
|
|
272
|
+
? retentionPolicy
|
|
273
|
+
: undefined,
|
|
274
|
+
workflowPolicy: workflowPolicy && Object.values(workflowPolicy).some((value) => value !== undefined)
|
|
275
|
+
? workflowPolicy
|
|
276
|
+
: undefined,
|
|
277
|
+
};
|
|
278
|
+
return overrides;
|
|
279
|
+
}
|
|
280
|
+
function readProjectConfigOverrides(phrenPath, projectName) {
|
|
281
|
+
try {
|
|
282
|
+
const config = readProjectConfig(phrenPath, projectName);
|
|
283
|
+
return normalizeProjectConfigOverrides(config.config);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export function getProjectConfigOverrides(phrenPath, projectName) {
|
|
290
|
+
return readProjectConfigOverrides(phrenPath, projectName) ?? null;
|
|
291
|
+
}
|
|
292
|
+
export function mergeConfig(phrenPath, projectName) {
|
|
293
|
+
const globalRetention = getRetentionPolicyGlobal(phrenPath);
|
|
294
|
+
const globalWorkflow = getWorkflowPolicyGlobal(phrenPath);
|
|
295
|
+
if (projectName && !isValidProjectName(projectName)) {
|
|
296
|
+
debugLog(`mergeConfig: invalid project name "${projectName}", using global defaults`);
|
|
297
|
+
projectName = undefined;
|
|
298
|
+
}
|
|
299
|
+
if (!projectName) {
|
|
300
|
+
return {
|
|
301
|
+
findingSensitivity: globalWorkflow.findingSensitivity,
|
|
302
|
+
proactivity: {},
|
|
303
|
+
taskMode: globalWorkflow.taskMode,
|
|
304
|
+
retentionPolicy: globalRetention,
|
|
305
|
+
workflowPolicy: globalWorkflow,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const overrides = readProjectConfigOverrides(phrenPath, projectName);
|
|
309
|
+
if (!overrides) {
|
|
310
|
+
return {
|
|
311
|
+
findingSensitivity: globalWorkflow.findingSensitivity,
|
|
312
|
+
proactivity: {},
|
|
313
|
+
taskMode: globalWorkflow.taskMode,
|
|
314
|
+
retentionPolicy: globalRetention,
|
|
315
|
+
workflowPolicy: globalWorkflow,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
// Merge retention policy
|
|
319
|
+
const retentionOverride = overrides.retentionPolicy;
|
|
320
|
+
const mergedRetention = retentionOverride
|
|
321
|
+
? {
|
|
322
|
+
schemaVersion: globalRetention.schemaVersion,
|
|
323
|
+
ttlDays: retentionOverride.ttlDays ?? globalRetention.ttlDays,
|
|
324
|
+
retentionDays: retentionOverride.retentionDays ?? globalRetention.retentionDays,
|
|
325
|
+
autoAcceptThreshold: retentionOverride.autoAcceptThreshold ?? globalRetention.autoAcceptThreshold,
|
|
326
|
+
minInjectConfidence: retentionOverride.minInjectConfidence ?? globalRetention.minInjectConfidence,
|
|
327
|
+
decay: {
|
|
328
|
+
d30: retentionOverride.decay?.d30 ?? globalRetention.decay.d30,
|
|
329
|
+
d60: retentionOverride.decay?.d60 ?? globalRetention.decay.d60,
|
|
330
|
+
d90: retentionOverride.decay?.d90 ?? globalRetention.decay.d90,
|
|
331
|
+
d120: retentionOverride.decay?.d120 ?? globalRetention.decay.d120,
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: globalRetention;
|
|
335
|
+
// Merge workflow policy
|
|
336
|
+
const workflowOverride = overrides.workflowPolicy;
|
|
337
|
+
const mergedWorkflow = {
|
|
338
|
+
schemaVersion: globalWorkflow.schemaVersion,
|
|
339
|
+
lowConfidenceThreshold: workflowOverride?.lowConfidenceThreshold ?? globalWorkflow.lowConfidenceThreshold,
|
|
340
|
+
riskySections: workflowOverride?.riskySections?.length
|
|
341
|
+
? workflowOverride.riskySections
|
|
342
|
+
: globalWorkflow.riskySections,
|
|
343
|
+
taskMode: overrides.taskMode ?? globalWorkflow.taskMode,
|
|
344
|
+
findingSensitivity: overrides.findingSensitivity ?? globalWorkflow.findingSensitivity,
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
findingSensitivity: mergedWorkflow.findingSensitivity,
|
|
348
|
+
proactivity: {
|
|
349
|
+
base: overrides.proactivity,
|
|
350
|
+
findings: overrides.proactivityFindings,
|
|
351
|
+
tasks: overrides.proactivityTask,
|
|
352
|
+
},
|
|
353
|
+
taskMode: mergedWorkflow.taskMode,
|
|
354
|
+
retentionPolicy: mergedRetention,
|
|
355
|
+
workflowPolicy: mergedWorkflow,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
215
358
|
function readJsonFile(filePath, fallback) {
|
|
216
359
|
try {
|
|
217
360
|
if (!fs.existsSync(filePath))
|
|
@@ -246,10 +389,15 @@ function writeJsonFile(filePath, data) {
|
|
|
246
389
|
writeJsonFileUnlocked(filePath, data);
|
|
247
390
|
});
|
|
248
391
|
}
|
|
249
|
-
|
|
392
|
+
function getRetentionPolicyGlobal(phrenPath) {
|
|
250
393
|
const parsed = readJsonFile(govFile(phrenPath, "retention-policy"), {});
|
|
251
394
|
return withDefaults(parsed, DEFAULT_POLICY);
|
|
252
395
|
}
|
|
396
|
+
export function getRetentionPolicy(phrenPath, projectName) {
|
|
397
|
+
if (projectName)
|
|
398
|
+
return mergeConfig(phrenPath, projectName).retentionPolicy;
|
|
399
|
+
return getRetentionPolicyGlobal(phrenPath);
|
|
400
|
+
}
|
|
253
401
|
export function updateRetentionPolicy(phrenPath, patch) {
|
|
254
402
|
const current = getRetentionPolicy(phrenPath);
|
|
255
403
|
const next = {
|
|
@@ -264,7 +412,7 @@ export function updateRetentionPolicy(phrenPath, patch) {
|
|
|
264
412
|
appendAuditLog(phrenPath, "update_policy", JSON.stringify(next));
|
|
265
413
|
return phrenOk(next);
|
|
266
414
|
}
|
|
267
|
-
|
|
415
|
+
function getWorkflowPolicyGlobal(phrenPath) {
|
|
268
416
|
const parsed = readJsonFile(govFile(phrenPath, "workflow-policy"), {});
|
|
269
417
|
const merged = withDefaults(parsed, DEFAULT_WORKFLOW_POLICY);
|
|
270
418
|
const validSections = new Set(["Review", "Stale", "Conflicts"]);
|
|
@@ -279,6 +427,11 @@ export function getWorkflowPolicy(phrenPath) {
|
|
|
279
427
|
}
|
|
280
428
|
return merged;
|
|
281
429
|
}
|
|
430
|
+
export function getWorkflowPolicy(phrenPath, projectName) {
|
|
431
|
+
if (projectName)
|
|
432
|
+
return mergeConfig(phrenPath, projectName).workflowPolicy;
|
|
433
|
+
return getWorkflowPolicyGlobal(phrenPath);
|
|
434
|
+
}
|
|
282
435
|
export function updateWorkflowPolicy(phrenPath, patch) {
|
|
283
436
|
const current = getWorkflowPolicy(phrenPath);
|
|
284
437
|
const riskySections = Array.isArray(patch.riskySections)
|
|
@@ -120,7 +120,7 @@ function readScoreJournal(phrenPath) {
|
|
|
120
120
|
return JSON.parse(line);
|
|
121
121
|
}
|
|
122
122
|
catch (err) {
|
|
123
|
-
if ((process.env.PHREN_DEBUG
|
|
123
|
+
if ((process.env.PHREN_DEBUG))
|
|
124
124
|
process.stderr.write(`[phren] readScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
@@ -153,7 +153,7 @@ function claimScoreJournal(phrenPath) {
|
|
|
153
153
|
return JSON.parse(line);
|
|
154
154
|
}
|
|
155
155
|
catch (err) {
|
|
156
|
-
if ((process.env.PHREN_DEBUG
|
|
156
|
+
if ((process.env.PHREN_DEBUG))
|
|
157
157
|
process.stderr.write(`[phren] claimScoreJournal parseLine: ${errorMessage(err)}\n`);
|
|
158
158
|
return null;
|
|
159
159
|
}
|
|
@@ -169,7 +169,7 @@ function claimScoreJournal(phrenPath) {
|
|
|
169
169
|
fs.unlinkSync(claimedFile);
|
|
170
170
|
}
|
|
171
171
|
catch (err) {
|
|
172
|
-
if ((process.env.PHREN_DEBUG
|
|
172
|
+
if ((process.env.PHREN_DEBUG))
|
|
173
173
|
process.stderr.write(`[phren] claimScoreJournal unlinkClaim: ${errorMessage(err)}\n`);
|
|
174
174
|
}
|
|
175
175
|
}
|