@phren/cli 0.0.10 → 0.0.12
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 +11 -17
- package/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-actions.js +58 -71
- package/mcp/dist/cli-config.js +337 -131
- package/mcp/dist/cli-extract.js +3 -2
- package/mcp/dist/cli-govern.js +35 -63
- package/mcp/dist/cli-graph.js +19 -4
- package/mcp/dist/cli-hooks-globs.js +2 -1
- package/mcp/dist/cli-hooks-output.js +4 -4
- package/mcp/dist/cli-hooks-session.js +1 -1
- package/mcp/dist/cli-hooks.js +44 -35
- package/mcp/dist/cli-namespaces.js +15 -5
- package/mcp/dist/cli-search.js +2 -2
- package/mcp/dist/cli.js +1 -1
- package/mcp/dist/content-archive.js +23 -14
- package/mcp/dist/content-citation.js +13 -2
- package/mcp/dist/content-dedup.js +9 -9
- package/mcp/dist/content-learning.js +6 -4
- package/mcp/dist/content-metadata.js +10 -0
- package/mcp/dist/core-finding.js +1 -1
- package/mcp/dist/data-access.js +10 -31
- package/mcp/dist/data-tasks.js +5 -26
- package/mcp/dist/embedding.js +7 -8
- package/mcp/dist/entrypoint.js +133 -102
- package/mcp/dist/finding-impact.js +1 -32
- package/mcp/dist/finding-journal.js +1 -1
- package/mcp/dist/finding-lifecycle.js +2 -7
- package/mcp/dist/governance-locks.js +12 -5
- package/mcp/dist/governance-policy.js +156 -9
- package/mcp/dist/governance-scores.js +4 -10
- package/mcp/dist/hooks.js +62 -18
- package/mcp/dist/index.js +4 -4
- package/mcp/dist/init-config.js +4 -25
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +6 -55
- package/mcp/dist/init-shared.js +53 -1
- package/mcp/dist/init.js +191 -29
- package/mcp/dist/link-checksums.js +3 -2
- package/mcp/dist/link-context.js +2 -2
- package/mcp/dist/link-doctor.js +14 -57
- package/mcp/dist/link-skills.js +98 -12
- package/mcp/dist/link.js +16 -75
- 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 +12 -7
- package/mcp/dist/mcp-extract.js +2 -2
- package/mcp/dist/mcp-finding.js +16 -20
- package/mcp/dist/mcp-graph.js +12 -12
- package/mcp/dist/mcp-hooks.js +1 -1
- package/mcp/dist/mcp-ops.js +18 -18
- package/mcp/dist/mcp-search.js +11 -16
- package/mcp/dist/mcp-session.js +12 -2
- package/mcp/dist/memory-ui-assets.js +1 -36
- package/mcp/dist/memory-ui-graph.js +152 -50
- package/mcp/dist/memory-ui-page.js +30 -5
- package/mcp/dist/memory-ui-scripts.js +252 -63
- package/mcp/dist/memory-ui-server.js +115 -3
- package/mcp/dist/phren-core.js +2 -0
- package/mcp/dist/phren-paths.js +8 -9
- package/mcp/dist/proactivity.js +5 -5
- package/mcp/dist/profile-store.js +2 -2
- package/mcp/dist/project-config.js +64 -17
- package/mcp/dist/provider-adapters.js +1 -1
- package/mcp/dist/query-correlation.js +22 -19
- package/mcp/dist/session-checkpoints.js +14 -14
- package/mcp/dist/session-utils.js +3 -2
- package/mcp/dist/shared-data-utils.js +28 -0
- package/mcp/dist/shared-fragment-graph.js +22 -21
- package/mcp/dist/shared-governance.js +1 -1
- package/mcp/dist/shared-index.js +144 -105
- package/mcp/dist/shared-retrieval.js +21 -23
- package/mcp/dist/shared-search-fallback.js +15 -25
- package/mcp/dist/shared-sqljs.js +3 -2
- package/mcp/dist/shared.js +5 -6
- package/mcp/dist/shell-entry.js +1 -1
- package/mcp/dist/shell-input.js +63 -53
- package/mcp/dist/shell-palette.js +6 -1
- package/mcp/dist/shell-render.js +9 -5
- package/mcp/dist/shell-state-store.js +2 -5
- package/mcp/dist/shell-view.js +7 -6
- package/mcp/dist/shell.js +5 -55
- package/mcp/dist/skill-files.js +4 -10
- package/mcp/dist/skill-registry.js +3 -0
- package/mcp/dist/status.js +43 -21
- 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 +4 -4
- package/package.json +2 -3
- package/skills/docs.md +11 -11
- package/starter/README.md +1 -1
- package/starter/global/CLAUDE.md +2 -2
- package/starter/global/skills/audit.md +106 -0
- package/mcp/dist/cli-hooks-retrieval.js +0 -2
- package/mcp/dist/impact-scoring.js +0 -22
- package/mcp/dist/shared-paths.js +0 -1
package/mcp/dist/entrypoint.js
CHANGED
|
@@ -5,110 +5,128 @@ 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 quickstart
|
|
13
|
-
phren add [path]
|
|
14
|
-
phren
|
|
15
|
-
|
|
16
|
-
phren
|
|
10
|
+
phren Interactive shell
|
|
11
|
+
phren init Set up phren
|
|
12
|
+
phren quickstart Quick setup: init + project scaffold
|
|
13
|
+
phren add [path] Register a project
|
|
14
|
+
phren search <query> Search what phren knows
|
|
15
|
+
phren status Health check
|
|
16
|
+
phren doctor [--fix] Diagnose and repair
|
|
17
|
+
phren web-ui Open the knowledge graph
|
|
18
|
+
phren tasks Cross-project task view
|
|
19
|
+
phren graph Fragment knowledge graph
|
|
20
|
+
|
|
21
|
+
phren help <topic> Detailed help for a topic
|
|
22
|
+
|
|
23
|
+
Topics: projects, skills, hooks, config, maintain, setup, env, all
|
|
24
|
+
`;
|
|
25
|
+
const HELP_TOPICS = {
|
|
26
|
+
projects: `Projects:
|
|
27
|
+
phren add [path] [--ownership <mode>] Register a project
|
|
28
|
+
phren projects list List all tracked projects
|
|
17
29
|
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
|
-
phren
|
|
31
|
-
phren
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
phren
|
|
35
|
-
|
|
36
|
-
phren
|
|
37
|
-
phren
|
|
38
|
-
phren
|
|
39
|
-
phren
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
phren
|
|
43
|
-
phren
|
|
44
|
-
phren
|
|
45
|
-
phren
|
|
46
|
-
phren
|
|
47
|
-
|
|
48
|
-
phren
|
|
49
|
-
|
|
30
|
+
Update per-project settings
|
|
31
|
+
phren projects remove <name> Remove a project
|
|
32
|
+
`,
|
|
33
|
+
skills: `Skills:
|
|
34
|
+
phren skills list List installed skills
|
|
35
|
+
phren skills add <project> <path> Link a skill into a project
|
|
36
|
+
phren skills show <name> Show skill content
|
|
37
|
+
phren skills resolve <project|global> Print resolved skill manifest
|
|
38
|
+
phren skills doctor <project|global> Diagnose skill visibility
|
|
39
|
+
phren skills sync <project|global> Regenerate skill mirror
|
|
40
|
+
phren skills enable <project|global> <name> Enable a disabled skill
|
|
41
|
+
phren skills disable <project|global> <name> Disable a skill without deleting
|
|
42
|
+
phren skills remove <project> <name> Remove a skill
|
|
43
|
+
phren detect-skills [--import] Find untracked skills in ~/.claude/skills/
|
|
44
|
+
`,
|
|
45
|
+
hooks: `Hooks:
|
|
46
|
+
phren hooks list [--project <name>] Show hook status per tool
|
|
47
|
+
phren hooks enable <tool> Enable hooks for a tool
|
|
48
|
+
phren hooks disable <tool> Disable hooks for a tool
|
|
49
|
+
phren hooks add-custom <event> <cmd> Add a custom hook
|
|
50
|
+
phren hooks remove-custom <event> Remove custom hooks
|
|
51
|
+
phren hooks errors [--limit <n>] Show recent hook errors
|
|
52
|
+
`,
|
|
53
|
+
config: `Configuration:
|
|
54
|
+
phren config show [--project <name>] Show current config
|
|
55
|
+
phren config policy [get|set ...] Retention, TTL, confidence, decay
|
|
56
|
+
phren config workflow [get|set ...] Risky-memory thresholds
|
|
57
|
+
phren config proactivity [level] Set proactivity level
|
|
58
|
+
phren config task-mode [mode] Set task automation mode
|
|
59
|
+
phren config finding-sensitivity [lvl] Set finding capture sensitivity
|
|
60
|
+
phren config index [get|set ...] Indexer include/exclude globs
|
|
61
|
+
phren config synonyms [list|add|remove] Manage learned synonyms
|
|
62
|
+
phren config project-ownership [mode] Default ownership for new projects
|
|
63
|
+
phren config machines Registered machines
|
|
64
|
+
phren config profiles Profiles and projects
|
|
65
|
+
phren config telemetry [on|off] Opt-in usage telemetry
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
phren
|
|
55
|
-
phren
|
|
56
|
-
|
|
57
|
-
phren
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
All config subcommands accept --project <name> for per-project overrides.
|
|
68
|
+
`,
|
|
69
|
+
maintain: `Maintenance:
|
|
70
|
+
phren maintain govern [project] Queue stale memories for review
|
|
71
|
+
phren maintain prune [project] Delete expired entries
|
|
72
|
+
phren maintain consolidate [project] Deduplicate findings
|
|
73
|
+
phren maintain extract [project] Mine git/GitHub signals
|
|
74
|
+
`,
|
|
75
|
+
setup: `Setup:
|
|
76
|
+
phren init [--mode shared|project-local] [--machine <n>] [--profile <n>] [--dry-run] [-y]
|
|
77
|
+
phren quickstart Quick setup: init + project scaffold
|
|
78
|
+
phren mcp-mode [on|off|status] Toggle MCP integration
|
|
79
|
+
phren hooks-mode [on|off|status] Toggle hook execution
|
|
80
|
+
phren verify Check init completed OK
|
|
81
|
+
phren uninstall Remove phren config and hooks
|
|
82
|
+
phren update [--refresh-starter] Update to latest version
|
|
83
|
+
`,
|
|
84
|
+
env: `Environment variables:
|
|
85
|
+
PHREN_PATH Override phren directory (default: ~/.phren)
|
|
86
|
+
PHREN_PROFILE Active profile name
|
|
87
|
+
PHREN_DEBUG Enable debug logging (set to 1)
|
|
60
88
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
Embeddings:
|
|
90
|
+
PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434, 'off' to disable)
|
|
91
|
+
PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint
|
|
92
|
+
PHREN_EMBEDDING_API_KEY API key for embedding endpoint
|
|
93
|
+
PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
phren uninstall Remove phren config and hooks
|
|
95
|
+
Context injection:
|
|
96
|
+
PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per prompt (default: 550)
|
|
97
|
+
PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
|
|
98
|
+
PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
|
|
72
99
|
|
|
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)
|
|
100
|
+
Feature flags:
|
|
101
|
+
PHREN_FEATURE_AUTO_EXTRACT=0 Disable auto memory extraction
|
|
102
|
+
PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations
|
|
103
|
+
PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
|
|
104
|
+
PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
phren
|
|
110
|
-
phren
|
|
111
|
-
|
|
106
|
+
Run 'phren help all' to see everything.
|
|
107
|
+
`,
|
|
108
|
+
};
|
|
109
|
+
function buildFullHelp() {
|
|
110
|
+
return `phren - persistent knowledge for your agents
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
phren Interactive shell
|
|
114
|
+
phren init Set up phren
|
|
115
|
+
phren quickstart Quick setup: init + project scaffold
|
|
116
|
+
phren add [path] Register a project
|
|
117
|
+
phren search <query> Search what phren knows
|
|
118
|
+
phren status Health check
|
|
119
|
+
phren doctor [--fix] Diagnose and repair
|
|
120
|
+
phren web-ui Open the knowledge graph
|
|
121
|
+
phren tasks Cross-project task view
|
|
122
|
+
phren graph Fragment knowledge graph
|
|
123
|
+
phren add-finding <p> "." Tell phren what you learned
|
|
124
|
+
phren pin <p> "..." Save a truth
|
|
125
|
+
phren review [project] Show review queue
|
|
126
|
+
phren session-context Current session state
|
|
127
|
+
|
|
128
|
+
${Object.values(HELP_TOPICS).join("\n")}`;
|
|
129
|
+
}
|
|
112
130
|
const CLI_COMMANDS = [
|
|
113
131
|
"search",
|
|
114
132
|
"shell",
|
|
@@ -216,7 +234,19 @@ async function promptProjectOwnership(phrenPath, fallback) {
|
|
|
216
234
|
export async function runTopLevelCommand(argv) {
|
|
217
235
|
const argvCommand = argv[0];
|
|
218
236
|
if (argvCommand === "--help" || argvCommand === "-h" || argvCommand === "help") {
|
|
219
|
-
|
|
237
|
+
const topic = argv[1]?.toLowerCase();
|
|
238
|
+
if (topic === "all") {
|
|
239
|
+
console.log(buildFullHelp());
|
|
240
|
+
}
|
|
241
|
+
else if (topic && HELP_TOPICS[topic]) {
|
|
242
|
+
console.log(HELP_TOPICS[topic]);
|
|
243
|
+
}
|
|
244
|
+
else if (topic) {
|
|
245
|
+
console.log(`Unknown topic: ${topic}\nAvailable: ${Object.keys(HELP_TOPICS).join(", ")}, all`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.log(HELP_TEXT);
|
|
249
|
+
}
|
|
220
250
|
return finish();
|
|
221
251
|
}
|
|
222
252
|
if (argvCommand === "add") {
|
|
@@ -313,7 +343,8 @@ export async function runTopLevelCommand(argv) {
|
|
|
313
343
|
}
|
|
314
344
|
if (argvCommand === "uninstall") {
|
|
315
345
|
const { runUninstall } = await import("./init.js");
|
|
316
|
-
|
|
346
|
+
const skipConfirm = argv.includes("--yes") || argv.includes("-y");
|
|
347
|
+
await runUninstall({ yes: skipConfirm });
|
|
317
348
|
return finish();
|
|
318
349
|
}
|
|
319
350
|
if (argvCommand === "status") {
|
|
@@ -349,7 +380,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
349
380
|
return finish();
|
|
350
381
|
}
|
|
351
382
|
catch (err) {
|
|
352
|
-
console.error(
|
|
383
|
+
console.error(errorMessage(err));
|
|
353
384
|
return finish(1);
|
|
354
385
|
}
|
|
355
386
|
}
|
|
@@ -360,7 +391,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
360
391
|
return finish();
|
|
361
392
|
}
|
|
362
393
|
catch (err) {
|
|
363
|
-
console.error(
|
|
394
|
+
console.error(errorMessage(err));
|
|
364
395
|
return finish(1);
|
|
365
396
|
}
|
|
366
397
|
}
|
|
@@ -383,7 +414,7 @@ export async function runTopLevelCommand(argv) {
|
|
|
383
414
|
trackCliCommand(defaultPhrenPath(), argvCommand);
|
|
384
415
|
}
|
|
385
416
|
catch (err) {
|
|
386
|
-
if ((process.env.PHREN_DEBUG
|
|
417
|
+
if ((process.env.PHREN_DEBUG))
|
|
387
418
|
process.stderr.write(`[phren] cli trackCliCommand: ${errorMessage(err)}\n`);
|
|
388
419
|
}
|
|
389
420
|
await runCliCommand(argvCommand, argv.slice(1));
|
|
@@ -2,19 +2,11 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import { impactLogFile } from "./shared.js";
|
|
4
4
|
import { withFileLock } from "./shared-governance.js";
|
|
5
|
+
import { normalizeFindingText } from "./content-metadata.js";
|
|
5
6
|
let highImpactCache = null;
|
|
6
7
|
function nowIso() {
|
|
7
8
|
return new Date().toISOString();
|
|
8
9
|
}
|
|
9
|
-
function normalizeFindingText(raw) {
|
|
10
|
-
return raw
|
|
11
|
-
.replace(/^-\s+/, "")
|
|
12
|
-
.replace(/<!--.*?-->/g, " ")
|
|
13
|
-
.replace(/\[confidence\s+[01](?:\.\d+)?\]/gi, " ")
|
|
14
|
-
.replace(/\s+/g, " ")
|
|
15
|
-
.trim()
|
|
16
|
-
.toLowerCase();
|
|
17
|
-
}
|
|
18
10
|
export function findingIdFromLine(line) {
|
|
19
11
|
const fid = line.match(/<!--\s*fid:([a-z0-9]{8})\s*-->/i);
|
|
20
12
|
if (fid?.[1])
|
|
@@ -144,29 +136,6 @@ export function getHighImpactFindings(phrenPath, minSurfaceCount = 3) {
|
|
|
144
136
|
};
|
|
145
137
|
return new Set(ids);
|
|
146
138
|
}
|
|
147
|
-
export function getImpactSurfaceCounts(phrenPath, minSurfaces = 1) {
|
|
148
|
-
const file = impactLogFile(phrenPath);
|
|
149
|
-
if (!fs.existsSync(file))
|
|
150
|
-
return new Map();
|
|
151
|
-
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
152
|
-
const counts = new Map();
|
|
153
|
-
for (const line of lines) {
|
|
154
|
-
try {
|
|
155
|
-
const entry = JSON.parse(line);
|
|
156
|
-
if (entry.findingId) {
|
|
157
|
-
counts.set(entry.findingId, (counts.get(entry.findingId) ?? 0) + 1);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
catch { }
|
|
161
|
-
}
|
|
162
|
-
// Filter by minimum
|
|
163
|
-
const filtered = new Map();
|
|
164
|
-
for (const [id, count] of counts) {
|
|
165
|
-
if (count >= minSurfaces)
|
|
166
|
-
filtered.set(id, count);
|
|
167
|
-
}
|
|
168
|
-
return filtered;
|
|
169
|
-
}
|
|
170
139
|
export function markImpactEntriesCompletedForSession(phrenPath, sessionId, project) {
|
|
171
140
|
if (!sessionId)
|
|
172
141
|
return 0;
|
|
@@ -105,7 +105,7 @@ export function compactFindingJournals(phrenPath, project) {
|
|
|
105
105
|
result.failed += 1;
|
|
106
106
|
continue;
|
|
107
107
|
}
|
|
108
|
-
if (
|
|
108
|
+
if (write.data.status === "skipped")
|
|
109
109
|
result.skipped += 1;
|
|
110
110
|
else
|
|
111
111
|
result.added += 1;
|
|
@@ -3,9 +3,9 @@ import * as path from "path";
|
|
|
3
3
|
import { PhrenError, phrenErr, phrenOk } from "./phren-core.js";
|
|
4
4
|
// Phren lifecycle comment prefix. No backward compat.
|
|
5
5
|
const LIFECYCLE_PREFIX = "phren";
|
|
6
|
-
import { withFileLock } from "./governance
|
|
6
|
+
import { withFileLock } from "./shared-governance.js";
|
|
7
7
|
import { isValidProjectName, safeProjectPath } from "./utils.js";
|
|
8
|
-
import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, } from "./content-metadata.js";
|
|
8
|
+
import { isArchiveEnd, isArchiveStart, parseCreatedDate as parseCreatedDateMeta, parseStatusField, parseStatus, parseSupersession, parseContradiction, parseFindingId as parseFindingIdMeta, stripLifecycleMetadata, stripRelationMetadata, normalizeFindingText, } from "./content-metadata.js";
|
|
9
9
|
export const FINDING_TYPE_DECAY = {
|
|
10
10
|
'pattern': { maxAgeDays: 365, decayMultiplier: 1.0 }, // Slow decay, long-lived
|
|
11
11
|
'decision': { maxAgeDays: Infinity, decayMultiplier: 1.0 }, // Never decays
|
|
@@ -101,11 +101,6 @@ function findingTextFromLine(line) {
|
|
|
101
101
|
.replace(/<!--.*?-->/g, "")
|
|
102
102
|
.trim();
|
|
103
103
|
}
|
|
104
|
-
function normalizeFindingText(value) {
|
|
105
|
-
return findingTextFromLine(value)
|
|
106
|
-
.replace(/\s+/g, " ")
|
|
107
|
-
.toLowerCase();
|
|
108
|
-
}
|
|
109
104
|
function removeRelationComments(line) {
|
|
110
105
|
return stripRelationMetadata(line);
|
|
111
106
|
}
|
|
@@ -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.
|
|
@@ -99,3 +100,9 @@ export function withFileLock(filePath, fn) {
|
|
|
99
100
|
releaseFileLock(lockPath);
|
|
100
101
|
return result;
|
|
101
102
|
}
|
|
103
|
+
export function isFiniteNumber(value) {
|
|
104
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
105
|
+
}
|
|
106
|
+
export function hasValidSchemaVersion(data) {
|
|
107
|
+
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
108
|
+
}
|
|
@@ -2,8 +2,9 @@ import * as crypto from "crypto";
|
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { appendAuditLog, debugLog, getProjectDirs, isRecord, runtimeHealthFile, withDefaults, phrenErr, PhrenError, phrenOk, resolveFindingsPath } from "./shared.js";
|
|
5
|
-
import { withFileLock } from "./governance
|
|
5
|
+
import { withFileLock, isFiniteNumber, hasValidSchemaVersion } from "./shared-governance.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;
|
|
@@ -43,15 +44,9 @@ function governanceDir(phrenPath) {
|
|
|
43
44
|
function govFile(phrenPath, schema) {
|
|
44
45
|
return path.join(governanceDir(phrenPath), GOVERNANCE_REGISTRY[schema].file);
|
|
45
46
|
}
|
|
46
|
-
function hasValidSchemaVersion(data) {
|
|
47
|
-
return !("schemaVersion" in data) || typeof data.schemaVersion === "number";
|
|
48
|
-
}
|
|
49
47
|
function isStringArray(value) {
|
|
50
48
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
51
49
|
}
|
|
52
|
-
function isFiniteNumber(value) {
|
|
53
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
54
|
-
}
|
|
55
50
|
function pickNumber(value, fallback) {
|
|
56
51
|
return isFiniteNumber(value) ? value : fallback;
|
|
57
52
|
}
|
|
@@ -212,6 +207,148 @@ function normalizeIndexPolicy(data) {
|
|
|
212
207
|
includeHidden: pickBoolean(data.includeHidden, DEFAULT_INDEX_POLICY.includeHidden),
|
|
213
208
|
};
|
|
214
209
|
}
|
|
210
|
+
const VALID_PROACTIVITY_LEVELS = ["high", "medium", "low"];
|
|
211
|
+
export const VALID_TASK_MODES = ["off", "manual", "suggest", "auto"];
|
|
212
|
+
export const VALID_FINDING_SENSITIVITY = ["minimal", "conservative", "balanced", "aggressive"];
|
|
213
|
+
const VALID_RISKY_SECTIONS = ["Review", "Stale", "Conflicts"];
|
|
214
|
+
function pickEnum(value, allowed) {
|
|
215
|
+
return typeof value === "string" && allowed.includes(value) ? value : undefined;
|
|
216
|
+
}
|
|
217
|
+
function pickPositiveInt(value) {
|
|
218
|
+
return Number.isInteger(value) && typeof value === "number" && value > 0 ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
function pickUnitInterval(value) {
|
|
221
|
+
return isFiniteNumber(value) && value >= 0 && value <= 1 ? value : undefined;
|
|
222
|
+
}
|
|
223
|
+
function normalizeProjectConfigOverrides(raw) {
|
|
224
|
+
if (!isRecord(raw))
|
|
225
|
+
return undefined;
|
|
226
|
+
const retentionRaw = isRecord(raw.retentionPolicy) ? raw.retentionPolicy : undefined;
|
|
227
|
+
const decayRaw = retentionRaw && isRecord(retentionRaw.decay) ? retentionRaw.decay : undefined;
|
|
228
|
+
const retentionPolicy = retentionRaw
|
|
229
|
+
? {
|
|
230
|
+
ttlDays: pickPositiveInt(retentionRaw.ttlDays),
|
|
231
|
+
retentionDays: pickPositiveInt(retentionRaw.retentionDays),
|
|
232
|
+
autoAcceptThreshold: pickUnitInterval(retentionRaw.autoAcceptThreshold),
|
|
233
|
+
minInjectConfidence: pickUnitInterval(retentionRaw.minInjectConfidence),
|
|
234
|
+
decay: decayRaw
|
|
235
|
+
? {
|
|
236
|
+
d30: pickUnitInterval(decayRaw.d30),
|
|
237
|
+
d60: pickUnitInterval(decayRaw.d60),
|
|
238
|
+
d90: pickUnitInterval(decayRaw.d90),
|
|
239
|
+
d120: pickUnitInterval(decayRaw.d120),
|
|
240
|
+
}
|
|
241
|
+
: undefined,
|
|
242
|
+
}
|
|
243
|
+
: undefined;
|
|
244
|
+
if (retentionPolicy && retentionPolicy.decay && Object.values(retentionPolicy.decay).every((value) => value === undefined)) {
|
|
245
|
+
delete retentionPolicy.decay;
|
|
246
|
+
}
|
|
247
|
+
const workflowRaw = isRecord(raw.workflowPolicy) ? raw.workflowPolicy : undefined;
|
|
248
|
+
const workflowPolicy = workflowRaw
|
|
249
|
+
? {
|
|
250
|
+
lowConfidenceThreshold: pickUnitInterval(workflowRaw.lowConfidenceThreshold),
|
|
251
|
+
riskySections: Array.isArray(workflowRaw.riskySections)
|
|
252
|
+
? workflowRaw.riskySections.filter((section) => typeof section === "string" && VALID_RISKY_SECTIONS.includes(section))
|
|
253
|
+
: undefined,
|
|
254
|
+
}
|
|
255
|
+
: undefined;
|
|
256
|
+
if (workflowPolicy && workflowPolicy.riskySections && workflowPolicy.riskySections.length === 0) {
|
|
257
|
+
delete workflowPolicy.riskySections;
|
|
258
|
+
}
|
|
259
|
+
const overrides = {
|
|
260
|
+
findingSensitivity: pickEnum(raw.findingSensitivity, VALID_FINDING_SENSITIVITY),
|
|
261
|
+
proactivity: pickEnum(raw.proactivity, VALID_PROACTIVITY_LEVELS),
|
|
262
|
+
proactivityFindings: pickEnum(raw.proactivityFindings, VALID_PROACTIVITY_LEVELS),
|
|
263
|
+
proactivityTask: pickEnum(raw.proactivityTask, VALID_PROACTIVITY_LEVELS),
|
|
264
|
+
taskMode: pickEnum(raw.taskMode, VALID_TASK_MODES),
|
|
265
|
+
retentionPolicy: retentionPolicy && Object.values(retentionPolicy).some((value) => value !== undefined)
|
|
266
|
+
? retentionPolicy
|
|
267
|
+
: undefined,
|
|
268
|
+
workflowPolicy: workflowPolicy && Object.values(workflowPolicy).some((value) => value !== undefined)
|
|
269
|
+
? workflowPolicy
|
|
270
|
+
: undefined,
|
|
271
|
+
};
|
|
272
|
+
return overrides;
|
|
273
|
+
}
|
|
274
|
+
function readProjectConfigOverrides(phrenPath, projectName) {
|
|
275
|
+
try {
|
|
276
|
+
const config = readProjectConfig(phrenPath, projectName);
|
|
277
|
+
return normalizeProjectConfigOverrides(config.config);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
export function getProjectConfigOverrides(phrenPath, projectName) {
|
|
284
|
+
return readProjectConfigOverrides(phrenPath, projectName) ?? null;
|
|
285
|
+
}
|
|
286
|
+
export function mergeConfig(phrenPath, projectName) {
|
|
287
|
+
const globalRetention = getRetentionPolicyGlobal(phrenPath);
|
|
288
|
+
const globalWorkflow = getWorkflowPolicyGlobal(phrenPath);
|
|
289
|
+
if (projectName && !isValidProjectName(projectName)) {
|
|
290
|
+
debugLog(`mergeConfig: invalid project name "${projectName}", using global defaults`);
|
|
291
|
+
projectName = undefined;
|
|
292
|
+
}
|
|
293
|
+
if (!projectName) {
|
|
294
|
+
return {
|
|
295
|
+
findingSensitivity: globalWorkflow.findingSensitivity,
|
|
296
|
+
proactivity: {},
|
|
297
|
+
taskMode: globalWorkflow.taskMode,
|
|
298
|
+
retentionPolicy: globalRetention,
|
|
299
|
+
workflowPolicy: globalWorkflow,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const overrides = readProjectConfigOverrides(phrenPath, projectName);
|
|
303
|
+
if (!overrides) {
|
|
304
|
+
return {
|
|
305
|
+
findingSensitivity: globalWorkflow.findingSensitivity,
|
|
306
|
+
proactivity: {},
|
|
307
|
+
taskMode: globalWorkflow.taskMode,
|
|
308
|
+
retentionPolicy: globalRetention,
|
|
309
|
+
workflowPolicy: globalWorkflow,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Merge retention policy
|
|
313
|
+
const retentionOverride = overrides.retentionPolicy;
|
|
314
|
+
const mergedRetention = retentionOverride
|
|
315
|
+
? {
|
|
316
|
+
schemaVersion: globalRetention.schemaVersion,
|
|
317
|
+
ttlDays: retentionOverride.ttlDays ?? globalRetention.ttlDays,
|
|
318
|
+
retentionDays: retentionOverride.retentionDays ?? globalRetention.retentionDays,
|
|
319
|
+
autoAcceptThreshold: retentionOverride.autoAcceptThreshold ?? globalRetention.autoAcceptThreshold,
|
|
320
|
+
minInjectConfidence: retentionOverride.minInjectConfidence ?? globalRetention.minInjectConfidence,
|
|
321
|
+
decay: {
|
|
322
|
+
d30: retentionOverride.decay?.d30 ?? globalRetention.decay.d30,
|
|
323
|
+
d60: retentionOverride.decay?.d60 ?? globalRetention.decay.d60,
|
|
324
|
+
d90: retentionOverride.decay?.d90 ?? globalRetention.decay.d90,
|
|
325
|
+
d120: retentionOverride.decay?.d120 ?? globalRetention.decay.d120,
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
: globalRetention;
|
|
329
|
+
// Merge workflow policy
|
|
330
|
+
const workflowOverride = overrides.workflowPolicy;
|
|
331
|
+
const mergedWorkflow = {
|
|
332
|
+
schemaVersion: globalWorkflow.schemaVersion,
|
|
333
|
+
lowConfidenceThreshold: workflowOverride?.lowConfidenceThreshold ?? globalWorkflow.lowConfidenceThreshold,
|
|
334
|
+
riskySections: workflowOverride?.riskySections?.length
|
|
335
|
+
? workflowOverride.riskySections
|
|
336
|
+
: globalWorkflow.riskySections,
|
|
337
|
+
taskMode: overrides.taskMode ?? globalWorkflow.taskMode,
|
|
338
|
+
findingSensitivity: overrides.findingSensitivity ?? globalWorkflow.findingSensitivity,
|
|
339
|
+
};
|
|
340
|
+
return {
|
|
341
|
+
findingSensitivity: mergedWorkflow.findingSensitivity,
|
|
342
|
+
proactivity: {
|
|
343
|
+
base: overrides.proactivity,
|
|
344
|
+
findings: overrides.proactivityFindings,
|
|
345
|
+
tasks: overrides.proactivityTask,
|
|
346
|
+
},
|
|
347
|
+
taskMode: mergedWorkflow.taskMode,
|
|
348
|
+
retentionPolicy: mergedRetention,
|
|
349
|
+
workflowPolicy: mergedWorkflow,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
215
352
|
function readJsonFile(filePath, fallback) {
|
|
216
353
|
try {
|
|
217
354
|
if (!fs.existsSync(filePath))
|
|
@@ -246,10 +383,15 @@ function writeJsonFile(filePath, data) {
|
|
|
246
383
|
writeJsonFileUnlocked(filePath, data);
|
|
247
384
|
});
|
|
248
385
|
}
|
|
249
|
-
|
|
386
|
+
function getRetentionPolicyGlobal(phrenPath) {
|
|
250
387
|
const parsed = readJsonFile(govFile(phrenPath, "retention-policy"), {});
|
|
251
388
|
return withDefaults(parsed, DEFAULT_POLICY);
|
|
252
389
|
}
|
|
390
|
+
export function getRetentionPolicy(phrenPath, projectName) {
|
|
391
|
+
if (projectName)
|
|
392
|
+
return mergeConfig(phrenPath, projectName).retentionPolicy;
|
|
393
|
+
return getRetentionPolicyGlobal(phrenPath);
|
|
394
|
+
}
|
|
253
395
|
export function updateRetentionPolicy(phrenPath, patch) {
|
|
254
396
|
const current = getRetentionPolicy(phrenPath);
|
|
255
397
|
const next = {
|
|
@@ -264,7 +406,7 @@ export function updateRetentionPolicy(phrenPath, patch) {
|
|
|
264
406
|
appendAuditLog(phrenPath, "update_policy", JSON.stringify(next));
|
|
265
407
|
return phrenOk(next);
|
|
266
408
|
}
|
|
267
|
-
|
|
409
|
+
function getWorkflowPolicyGlobal(phrenPath) {
|
|
268
410
|
const parsed = readJsonFile(govFile(phrenPath, "workflow-policy"), {});
|
|
269
411
|
const merged = withDefaults(parsed, DEFAULT_WORKFLOW_POLICY);
|
|
270
412
|
const validSections = new Set(["Review", "Stale", "Conflicts"]);
|
|
@@ -279,6 +421,11 @@ export function getWorkflowPolicy(phrenPath) {
|
|
|
279
421
|
}
|
|
280
422
|
return merged;
|
|
281
423
|
}
|
|
424
|
+
export function getWorkflowPolicy(phrenPath, projectName) {
|
|
425
|
+
if (projectName)
|
|
426
|
+
return mergeConfig(phrenPath, projectName).workflowPolicy;
|
|
427
|
+
return getWorkflowPolicyGlobal(phrenPath);
|
|
428
|
+
}
|
|
282
429
|
export function updateWorkflowPolicy(phrenPath, patch) {
|
|
283
430
|
const current = getWorkflowPolicy(phrenPath);
|
|
284
431
|
const riskySections = Array.isArray(patch.riskySections)
|