@phren/cli 0.0.16 → 0.0.18

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.
@@ -552,4 +552,106 @@ export const TASK_UI_STYLES = `
552
552
  transition: transform 0.2s;
553
553
  }
554
554
  .task-done-list { padding-top: 8px; }
555
+
556
+ /* ── Task Summary Bar ──────────────────────────── */
557
+ .task-summary-bar {
558
+ display: flex;
559
+ align-items: center;
560
+ gap: 10px;
561
+ padding: 10px 14px;
562
+ background: var(--surface-sunken, var(--surface));
563
+ border: 1px solid var(--border);
564
+ border-radius: var(--radius);
565
+ margin-bottom: 16px;
566
+ flex-wrap: wrap;
567
+ font-size: var(--text-sm);
568
+ }
569
+ .task-summary-total {
570
+ font-weight: 600;
571
+ color: var(--ink);
572
+ font-size: var(--text-base);
573
+ }
574
+ .task-summary-pill {
575
+ display: inline-flex;
576
+ align-items: center;
577
+ padding: 2px 8px;
578
+ border-radius: 999px;
579
+ font-size: 11px;
580
+ font-weight: 600;
581
+ }
582
+ .task-summary-high { background: #ef444422; color: #ef4444; }
583
+ .task-summary-medium { background: #f59e0b22; color: #f59e0b; }
584
+ .task-summary-low { background: #6b728022; color: #6b7280; }
585
+ .task-summary-projects {
586
+ display: flex;
587
+ gap: 6px;
588
+ align-items: center;
589
+ }
590
+ .task-summary-project {
591
+ font-size: 11px;
592
+ color: var(--muted);
593
+ padding: 1px 6px;
594
+ border: 1px solid var(--border);
595
+ border-radius: var(--radius-sm);
596
+ }
597
+
598
+ /* ── Task Session Badge ──────────────────────────── */
599
+ .task-session-badge {
600
+ display: inline-block;
601
+ padding: 1px 6px;
602
+ border-radius: var(--radius-sm);
603
+ font-size: 10px;
604
+ font-family: var(--mono, monospace);
605
+ background: var(--surface-sunken, var(--surface));
606
+ color: var(--muted);
607
+ border: 1px solid var(--border);
608
+ }
609
+
610
+ /* ── Task View Toggle ──────────────────────────── */
611
+ .task-view-toggle {
612
+ display: flex;
613
+ gap: 2px;
614
+ border: 1px solid var(--border);
615
+ border-radius: var(--radius-sm);
616
+ overflow: hidden;
617
+ }
618
+ .task-view-btn {
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: center;
622
+ width: 30px;
623
+ height: 28px;
624
+ background: var(--surface);
625
+ border: none;
626
+ color: var(--muted);
627
+ cursor: pointer;
628
+ transition: background 0.15s, color 0.15s;
629
+ }
630
+ .task-view-btn:hover { color: var(--ink); }
631
+ .task-view-btn.active {
632
+ background: var(--accent-dim);
633
+ color: var(--accent);
634
+ }
635
+
636
+ /* ── Task Compact Grid ──────────────────────────── */
637
+ .task-card-grid-compact {
638
+ display: grid !important;
639
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
640
+ gap: 8px;
641
+ }
642
+ .task-card-grid-compact .task-card {
643
+ padding: 10px 12px;
644
+ }
645
+ .task-card-grid-compact .task-card-body {
646
+ gap: 2px;
647
+ }
648
+ .task-card-grid-compact .task-card-text {
649
+ font-size: var(--text-sm);
650
+ display: -webkit-box;
651
+ -webkit-line-clamp: 2;
652
+ -webkit-box-orient: vertical;
653
+ overflow: hidden;
654
+ }
655
+ .task-card-grid-compact .task-card-context { display: none; }
656
+ .task-card-grid-compact .task-card-actions { margin-top: 6px; }
555
657
  `;
@@ -25,7 +25,7 @@ export const EXTRA_ENTITY_PATTERNS = [
25
25
  { re: /\b\d{4}[-/]\d{2}[-/]\d{2}\b/g, label: "date" },
26
26
  ];
27
27
  /** Union of all directory names reserved by phren infrastructure — not valid project names. */
28
- export const RESERVED_PROJECT_DIR_NAMES = new Set(["global", ".runtime", ".sessions", ".governance", "profiles", "templates"]);
28
+ export const RESERVED_PROJECT_DIR_NAMES = new Set(["global", ".runtime", ".sessions", ".config", "profiles", "templates"]);
29
29
  // Default timeout for execFileSync calls (30s for most operations, 10s for quick probes like `which`)
30
30
  export const EXEC_TIMEOUT_MS = 30_000;
31
31
  export const EXEC_TIMEOUT_QUICK_MS = 10_000;
@@ -108,7 +108,7 @@ function hasRootManifest(candidate) {
108
108
  }
109
109
  function hasInstallMarkers(candidate) {
110
110
  return fs.existsSync(path.join(candidate, "machines.yaml"))
111
- || fs.existsSync(path.join(candidate, ".governance"))
111
+ || fs.existsSync(path.join(candidate, ".config"))
112
112
  || fs.existsSync(path.join(candidate, "global"));
113
113
  }
114
114
  function isPhrenRootCandidate(candidate) {
@@ -460,7 +460,7 @@ export function computePhrenLiveStateToken(phrenPath) {
460
460
  pushDirTokens(parts, path.join(phrenPath, "profiles"));
461
461
  }
462
462
  pushDirTokens(parts, path.join(phrenPath, "global", "skills"));
463
- pushFileToken(parts, path.join(phrenPath, ".governance", "access-control.json"));
463
+ pushFileToken(parts, path.join(phrenPath, ".config", "access-control.json"));
464
464
  pushFileToken(parts, rootManifestPath(phrenPath));
465
465
  pushFileToken(parts, runtimeHealthFile(phrenPath));
466
466
  pushFileToken(parts, runtimeFile(phrenPath, "audit.log"));
@@ -20,6 +20,37 @@ function parseProactivityLevel(raw) {
20
20
  function resolveProactivityPhrenPath(explicitPhrenPath) {
21
21
  return explicitPhrenPath ?? findPhrenPath();
22
22
  }
23
+ /** Read per-user preferences from ~/.phren/.users/<actor>/preferences.json. Actor from PHREN_ACTOR env var. */
24
+ export function readUserPreferences(explicitPhrenPath) {
25
+ const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
26
+ if (!phrenPath)
27
+ return {};
28
+ const actor = (process.env.PHREN_ACTOR || "").trim();
29
+ if (!actor || !/^[a-zA-Z0-9_@.-]{1,128}$/.test(actor))
30
+ return {};
31
+ // Sanitize actor name to safe path component (no path traversal)
32
+ const safeActor = actor.replace(/[^a-zA-Z0-9_@.-]/g, "_");
33
+ const prefsFile = `${phrenPath}/.users/${safeActor}/preferences.json`;
34
+ if (!fs.existsSync(prefsFile))
35
+ return {};
36
+ try {
37
+ const parsed = JSON.parse(fs.readFileSync(prefsFile, "utf8"));
38
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
39
+ return {};
40
+ return {
41
+ proactivity: parseProactivityLevel(parsed.proactivity),
42
+ proactivityFindings: parseProactivityLevel(parsed.proactivityFindings),
43
+ proactivityTask: parseProactivityLevel(parsed.proactivityTask),
44
+ findingSensitivity: ["minimal", "conservative", "balanced", "aggressive"].includes(String(parsed.findingSensitivity))
45
+ ? parsed.findingSensitivity
46
+ : undefined,
47
+ };
48
+ }
49
+ catch (err) {
50
+ debugLog(`readUserPreferences: failed to parse ${prefsFile}: ${errorMessage(err)}`);
51
+ return {};
52
+ }
53
+ }
23
54
  function readGovernanceProactivityPreferences(explicitPhrenPath) {
24
55
  const phrenPath = resolveProactivityPhrenPath(explicitPhrenPath);
25
56
  if (!phrenPath)
@@ -41,6 +72,10 @@ function readGovernanceProactivityPreferences(explicitPhrenPath) {
41
72
  return {};
42
73
  }
43
74
  function getConfiguredProactivityDefault(explicitPhrenPath) {
75
+ // Resolution chain: user prefs (highest) → governance prefs → install prefs → default
76
+ const userPrefs = readUserPreferences(explicitPhrenPath);
77
+ if (userPrefs.proactivity)
78
+ return userPrefs.proactivity;
44
79
  const governancePreference = readGovernanceProactivityPreferences(explicitPhrenPath).proactivity;
45
80
  if (governancePreference)
46
81
  return governancePreference;
@@ -75,6 +110,12 @@ function getWorkflowPolicySensitivityLevel(explicitPhrenPath) {
75
110
  }
76
111
  }
77
112
  function getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath) {
113
+ // User prefs take priority over governance prefs
114
+ const userPrefs = readUserPreferences(explicitPhrenPath);
115
+ if (userPrefs.proactivityFindings)
116
+ return userPrefs.proactivityFindings;
117
+ if (userPrefs.proactivity)
118
+ return userPrefs.proactivity;
78
119
  const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
79
120
  return prefs.proactivityFindings
80
121
  ?? prefs.proactivity
@@ -82,6 +123,12 @@ function getConfiguredProactivityLevelForFindingsDefault(explicitPhrenPath) {
82
123
  ?? getConfiguredProactivityDefault(explicitPhrenPath);
83
124
  }
84
125
  function getConfiguredProactivityLevelForTaskDefault(explicitPhrenPath) {
126
+ // User prefs take priority over governance prefs
127
+ const userPrefs = readUserPreferences(explicitPhrenPath);
128
+ if (userPrefs.proactivityTask)
129
+ return userPrefs.proactivityTask;
130
+ if (userPrefs.proactivity)
131
+ return userPrefs.proactivity;
85
132
  const prefs = readGovernanceProactivityPreferences(explicitPhrenPath);
86
133
  return prefs.proactivityTask
87
134
  ?? prefs.proactivity
@@ -116,6 +116,104 @@ export function setMachineProfile(phrenPath, machine, profile) {
116
116
  return phrenOk(`Mapped machine ${machine} -> ${profile}.`);
117
117
  });
118
118
  }
119
+ const VALID_FINDING_SENSITIVITY_VALS = ["minimal", "conservative", "balanced", "aggressive"];
120
+ const VALID_PROACTIVITY_VALS = ["high", "medium", "low"];
121
+ const VALID_TASK_MODE_VALS = ["off", "manual", "suggest", "auto"];
122
+ const VALID_RISKY_SECTION_VALS = ["Review", "Stale", "Conflicts"];
123
+ function pickEnumVal(value, allowed) {
124
+ return typeof value === "string" && allowed.includes(value) ? value : undefined;
125
+ }
126
+ function pickFiniteNum(value) {
127
+ return typeof value === "number" && isFinite(value) ? value : undefined;
128
+ }
129
+ function parseProfilePolicyDefaults(raw) {
130
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
131
+ return undefined;
132
+ const data = raw;
133
+ const retentionRaw = data.retentionPolicy && typeof data.retentionPolicy === "object" && !Array.isArray(data.retentionPolicy)
134
+ ? data.retentionPolicy
135
+ : undefined;
136
+ const decayRaw = retentionRaw?.decay && typeof retentionRaw.decay === "object" && !Array.isArray(retentionRaw.decay)
137
+ ? retentionRaw.decay
138
+ : undefined;
139
+ const workflowRaw = data.workflowPolicy && typeof data.workflowPolicy === "object" && !Array.isArray(data.workflowPolicy)
140
+ ? data.workflowPolicy
141
+ : undefined;
142
+ const result = {};
143
+ const fs_ = pickEnumVal(data.findingSensitivity, VALID_FINDING_SENSITIVITY_VALS);
144
+ if (fs_)
145
+ result.findingSensitivity = fs_;
146
+ const pr = pickEnumVal(data.proactivity, VALID_PROACTIVITY_VALS);
147
+ if (pr)
148
+ result.proactivity = pr;
149
+ const prf = pickEnumVal(data.proactivityFindings, VALID_PROACTIVITY_VALS);
150
+ if (prf)
151
+ result.proactivityFindings = prf;
152
+ const prt = pickEnumVal(data.proactivityTask, VALID_PROACTIVITY_VALS);
153
+ if (prt)
154
+ result.proactivityTask = prt;
155
+ const tm = pickEnumVal(data.taskMode, VALID_TASK_MODE_VALS);
156
+ if (tm)
157
+ result.taskMode = tm;
158
+ if (retentionRaw) {
159
+ const ret = {};
160
+ const ttlDays = pickFiniteNum(retentionRaw.ttlDays);
161
+ if (ttlDays !== undefined)
162
+ ret.ttlDays = ttlDays;
163
+ const retentionDays = pickFiniteNum(retentionRaw.retentionDays);
164
+ if (retentionDays !== undefined)
165
+ ret.retentionDays = retentionDays;
166
+ const aat = pickFiniteNum(retentionRaw.autoAcceptThreshold);
167
+ if (aat !== undefined)
168
+ ret.autoAcceptThreshold = aat;
169
+ const mic = pickFiniteNum(retentionRaw.minInjectConfidence);
170
+ if (mic !== undefined)
171
+ ret.minInjectConfidence = mic;
172
+ if (decayRaw) {
173
+ const decay = {};
174
+ const d30 = pickFiniteNum(decayRaw.d30);
175
+ if (d30 !== undefined)
176
+ decay.d30 = d30;
177
+ const d60 = pickFiniteNum(decayRaw.d60);
178
+ if (d60 !== undefined)
179
+ decay.d60 = d60;
180
+ const d90 = pickFiniteNum(decayRaw.d90);
181
+ if (d90 !== undefined)
182
+ decay.d90 = d90;
183
+ const d120 = pickFiniteNum(decayRaw.d120);
184
+ if (d120 !== undefined)
185
+ decay.d120 = d120;
186
+ if (Object.keys(decay).length > 0)
187
+ ret.decay = decay;
188
+ }
189
+ if (Object.keys(ret).length > 0)
190
+ result.retentionPolicy = ret;
191
+ }
192
+ if (workflowRaw) {
193
+ const wf = {};
194
+ const lct = pickFiniteNum(workflowRaw.lowConfidenceThreshold);
195
+ if (lct !== undefined)
196
+ wf.lowConfidenceThreshold = lct;
197
+ if (Array.isArray(workflowRaw.riskySections)) {
198
+ const rs = workflowRaw.riskySections
199
+ .filter((s) => typeof s === "string" && VALID_RISKY_SECTION_VALS.includes(s));
200
+ if (rs.length > 0)
201
+ wf.riskySections = rs;
202
+ }
203
+ if (Object.keys(wf).length > 0)
204
+ result.workflowPolicy = wf;
205
+ }
206
+ return Object.keys(result).length > 0 ? result : undefined;
207
+ }
208
+ export function getActiveProfileDefaults(phrenPath, profile) {
209
+ const profiles = listProfiles(phrenPath);
210
+ if (!profiles.ok)
211
+ return undefined;
212
+ const activeName = profile ?? profiles.data[0]?.name;
213
+ if (!activeName)
214
+ return undefined;
215
+ return profiles.data.find((p) => p.name === activeName)?.defaults;
216
+ }
119
217
  export function listProfiles(phrenPath) {
120
218
  const profilesDir = path.join(phrenPath, "profiles");
121
219
  if (!fs.existsSync(profilesDir))
@@ -136,7 +234,8 @@ export function listProfiles(phrenPath) {
136
234
  const projects = Array.isArray(data?.projects)
137
235
  ? data.projects.map((project) => String(project)).filter(Boolean)
138
236
  : [];
139
- profiles.push({ name, file: full, projects });
237
+ const defaults = parseProfilePolicyDefaults(data?.defaults);
238
+ profiles.push({ name, file: full, projects, ...(defaults ? { defaults } : {}) });
140
239
  }
141
240
  catch (err) {
142
241
  if ((process.env.PHREN_DEBUG))
@@ -303,7 +303,7 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
303
303
  process.stderr.write(`[phren] computePhrenHash skip: ${errorMessage(err)}\n`);
304
304
  }
305
305
  }
306
- const indexPolicyPath = path.join(phrenPath, ".governance", "index-policy.json");
306
+ const indexPolicyPath = path.join(phrenPath, ".config", "index-policy.json");
307
307
  if (fs.existsSync(indexPolicyPath)) {
308
308
  try {
309
309
  const stat = fs.statSync(indexPolicyPath);
@@ -683,7 +683,7 @@ function isSentinelFresh(phrenPath, sentinel) {
683
683
  // Check mtime of key directories — if any are newer than the sentinel, it's stale
684
684
  const dirsToCheck = [
685
685
  phrenPath,
686
- path.join(phrenPath, ".governance"),
686
+ path.join(phrenPath, ".config"),
687
687
  path.join(phrenPath, ".runtime"),
688
688
  ];
689
689
  for (const dir of dirsToCheck) {
@@ -1,6 +1,6 @@
1
1
  // shared-retrieval.ts — shared retrieval core used by hooks and MCP search.
2
2
  import { getQualityMultiplier, entryScoreKey, } from "./shared-governance.js";
3
- import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getEntityBoostDocs, decodeFiniteNumber, rowToDocWithRowid, } from "./shared-index.js";
3
+ import { queryDocRows, queryRows, cosineFallback, extractSnippet, getDocSourceKey, getEntityBoostDocs, decodeFiniteNumber, rowToDocWithRowid, buildIndex, } from "./shared-index.js";
4
4
  import { filterTrustedFindingsDetailed, } from "./shared-content.js";
5
5
  import { parseCitationComment } from "./content-citation.js";
6
6
  import { getHighImpactFindings } from "./finding-impact.js";
@@ -490,6 +490,47 @@ export async function searchKnowledgeRows(db, options) {
490
490
  }
491
491
  return { safeQuery, rows, usedFallback };
492
492
  }
493
+ /**
494
+ * Parse PHREN_FEDERATION_PATHS env var and return valid, distinct paths.
495
+ * Paths are colon-separated. The local phrenPath is excluded to avoid duplicate results.
496
+ */
497
+ export function parseFederationPaths(localPhrenPath) {
498
+ const raw = process.env.PHREN_FEDERATION_PATHS ?? "";
499
+ if (!raw.trim())
500
+ return [];
501
+ return raw
502
+ .split(":")
503
+ .map((p) => p.trim())
504
+ .filter((p) => p.length > 0 && p !== localPhrenPath && fs.existsSync(p));
505
+ }
506
+ /**
507
+ * Search additional phren stores defined in PHREN_FEDERATION_PATHS.
508
+ * Returns an array of results tagged with their source store. Read-only — no mutations.
509
+ */
510
+ export async function searchFederatedStores(localPhrenPath, options) {
511
+ const federationPaths = parseFederationPaths(localPhrenPath);
512
+ if (federationPaths.length === 0)
513
+ return [];
514
+ const allRows = [];
515
+ for (const storePath of federationPaths) {
516
+ try {
517
+ const federatedDb = await buildIndex(storePath);
518
+ const result = await searchKnowledgeRows(federatedDb, { ...options, phrenPath: storePath });
519
+ if (result.rows && result.rows.length > 0) {
520
+ for (const row of result.rows) {
521
+ allRows.push({ ...row, federationSource: storePath });
522
+ }
523
+ }
524
+ }
525
+ catch (err) {
526
+ if (process.env.PHREN_DEBUG) {
527
+ process.stderr.write(`[phren] federatedSearch storePath=${storePath}: ${errorMessage(err)}\n`);
528
+ }
529
+ // Federation errors are non-fatal — continue with other stores
530
+ }
531
+ }
532
+ return allRows;
533
+ }
493
534
  // ── Trust filter ─────────────────────────────────────────────────────────────
494
535
  const TRUST_FILTERED_TYPES = new Set(["findings", "reference", "knowledge"]);
495
536
  /** Apply trust filter to rows. Returns filtered rows plus any queue/audit items to be written
@@ -383,7 +383,7 @@ export async function executePalette(host, input) {
383
383
  process.stderr.write(`[phren] shell status gitStatus: ${errorMessage(err)}\n`);
384
384
  }
385
385
  const auditPathNew = runtimeFile(host.phrenPath, "audit.log");
386
- const auditPathLegacy = path.join(host.phrenPath, ".governance", "audit.log");
386
+ const auditPathLegacy = path.join(host.phrenPath, ".config", "audit.log");
387
387
  const auditPath = fs.existsSync(auditPathNew) ? auditPathNew : auditPathLegacy;
388
388
  if (fs.existsSync(auditPath)) {
389
389
  const auditLines = fs.readFileSync(auditPath, "utf8").split("\n")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {