pi-orchestrator-config 4.10.0 → 4.11.0

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.
@@ -26,7 +26,7 @@ import { setSlot } from "./status-bar.js";
26
26
  import { rebuildAndOrganize } from "./situation-report.js";
27
27
  import { runPromotionPass } from "./memory-promotion.js";
28
28
  import { mergeProvenancePending } from "./memory-provenance.js";
29
- import { getProjectTmpDir, shouldSkipOneshotShutdownDream } from "./utils.js";
29
+ import { getProjectTmpDir, getProjectMemoryDir, shouldSkipOneshotShutdownDream } from "./utils.js";
30
30
  import { fileLog } from "../shared/file-logger.js";
31
31
 
32
32
  // Scoring rebuild runs every 30 minutes (cheap, no LLM — just rescores and reorganizes)
@@ -101,7 +101,7 @@ export function registerDreaming(
101
101
  updateDreamStatus();
102
102
  currentDreamId = ""; // Reset until we get the ID from spawnAsyncAgent
103
103
  const { agents } = discoverAgents(cwd, "user");
104
- const topicsDir = path.join(cwd, ".pi", "memory", "topics");
104
+ const topicsDir = path.join(getProjectMemoryDir(cwd), "topics");
105
105
  const { id } = spawnAsyncAgent(
106
106
  "worker",
107
107
  `Memory dreaming — analyze session and maintain topic files.\n` +
@@ -37,6 +37,7 @@ import { registerSessionSearch } from "./session-search.js";
37
37
  import { registerSettingsTui } from "./settings-tui.js";
38
38
  import { registerMarkdownTransformer } from "./markdown-transformer.js";
39
39
  import { registerGraft } from "./graft.js";
40
+ import { registerReviewLearning } from "./review-learning.js";
40
41
  import { ensureGitSshTimeout, isRunningInContainer, terminalNotify } from "./utils.js";
41
42
  import { createLogger } from "../shared/logger.js";
42
43
  import { registerProviderStreamLog } from "../shared/provider-stream-log.js";
@@ -112,6 +113,7 @@ export default function (pi: ExtensionAPI) {
112
113
  registerNvim(pi);
113
114
  registerPreferenceExtractor(pi);
114
115
  registerMemoryTools(pi);
116
+ registerReviewLearning(pi);
115
117
  registerSessionSearch(pi);
116
118
  registerSettingsTui(pi);
117
119
  registerMarkdownTransformer(pi);
@@ -14,6 +14,7 @@ import { join } from "node:path";
14
14
  import { createHash } from "node:crypto";
15
15
  import { createCachedStore } from "./state-jsonl.js";
16
16
  import type { JsonlStateStore } from "./state-jsonl.js";
17
+ import { getProjectMemoryDir } from "./utils.js";
17
18
 
18
19
  const EMBEDDING_DIM = 384;
19
20
  export const EMBEDDING_POOLING = "mean";
@@ -42,12 +43,12 @@ const LEGACY_STORE_FILENAME = "embeddings.json";
42
43
 
43
44
  function getEmbeddingStore(cwd: string): JsonlStateStore<EmbeddingStore> {
44
45
  return createCachedStore<EmbeddingStore>(
45
- join(cwd, ".pi", "memory"), STORE_FILENAME_JSONL, LEGACY_STORE_FILENAME, { compactThreshold: 30 },
46
+ getProjectMemoryDir(cwd), STORE_FILENAME_JSONL, LEGACY_STORE_FILENAME, { compactThreshold: 30 },
46
47
  );
47
48
  }
48
49
 
49
50
  function getStorePath(cwd: string): string {
50
- return join(cwd, ".pi", "memory", STORE_FILENAME_JSONL);
51
+ return join(getProjectMemoryDir(cwd), STORE_FILENAME_JSONL);
51
52
  }
52
53
 
53
54
  function loadStore(cwd: string): EmbeddingStore {
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
11
  import { createLogger } from "../shared/logger.js";
12
+ import { getProjectMemoryDir } from "./utils.js";
12
13
  import {
13
14
  type EnforcementAction,
14
15
  type EnforcementTrigger,
@@ -159,7 +160,7 @@ export function markTopicEntryEnforced(
159
160
  ): boolean {
160
161
  const topicName = CATEGORY_TO_TOPIC[category];
161
162
  if (!topicName) return false;
162
- const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
163
+ const topicPath = join(getProjectMemoryDir(cwd), "topics", `${topicName}.md`);
163
164
  if (!existsSync(topicPath)) return false;
164
165
 
165
166
  let content = readFileSync(topicPath, "utf-8");
@@ -13,6 +13,7 @@ import { join } from "node:path";
13
13
  import { entryHash, mutateScores, type MemoryCategory } from "./memory-scoring.js";
14
14
  import { createCachedStore } from "./state-jsonl.js";
15
15
  import type { JsonlStateStore } from "./state-jsonl.js";
16
+ import { getProjectMemoryDir } from "./utils.js";
16
17
 
17
18
  export interface ProvenancePendingItem {
18
19
  category: MemoryCategory | string;
@@ -31,12 +32,12 @@ const LEGACY_PROVENANCE_FILENAME = "provenance-pending.json";
31
32
 
32
33
  function getProvenanceStore(cwd: string): JsonlStateStore<ProvenancePendingFile> {
33
34
  return createCachedStore<ProvenancePendingFile>(
34
- join(cwd, ".pi", "memory"), PROVENANCE_FILENAME_JSONL, LEGACY_PROVENANCE_FILENAME,
35
+ getProjectMemoryDir(cwd), PROVENANCE_FILENAME_JSONL, LEGACY_PROVENANCE_FILENAME,
35
36
  );
36
37
  }
37
38
 
38
39
  export function getProvenancePendingPath(cwd: string): string {
39
- return join(cwd, ".pi", "memory", PROVENANCE_FILENAME_JSONL);
40
+ return join(getProjectMemoryDir(cwd), PROVENANCE_FILENAME_JSONL);
40
41
  }
41
42
 
42
43
  export function writeProvenancePending(
@@ -16,6 +16,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
16
16
  import { createCachedStore } from "./state-jsonl.js";
17
17
  import type { JsonlStateStore } from "./state-jsonl.js";
18
18
  import { getSetting } from "./project-settings.js";
19
+ import { getProjectMemoryDir } from "./utils.js";
19
20
 
20
21
  // ── Types ──────────────────────────────────────────────────────────────────
21
22
 
@@ -246,12 +247,12 @@ const LEGACY_SCORES_FILENAME = "memory-scores.json";
246
247
 
247
248
  function getScoresStore(cwd: string): JsonlStateStore<ScoresFile> {
248
249
  return createCachedStore<ScoresFile>(
249
- join(cwd, ".pi", "memory"), SCORES_FILENAME_JSONL, LEGACY_SCORES_FILENAME, { compactThreshold: 50 },
250
+ getProjectMemoryDir(cwd), SCORES_FILENAME_JSONL, LEGACY_SCORES_FILENAME, { compactThreshold: 50 },
250
251
  );
251
252
  }
252
253
 
253
254
  export function getScoresPath(cwd: string): string {
254
- return join(cwd, ".pi", "memory", SCORES_FILENAME_JSONL);
255
+ return join(getProjectMemoryDir(cwd), SCORES_FILENAME_JSONL);
255
256
  }
256
257
 
257
258
  /**
@@ -320,7 +321,7 @@ export function loadScores(cwd: string): ScoresFile {
320
321
  * imports loadScores/entryHash from this module).
321
322
  */
322
323
  function parseTopicLinesForMigration(cwd: string): { category: string; text: string }[] {
323
- const topicsDir = join(cwd, ".pi", "memory", "topics");
324
+ const topicsDir = join(getProjectMemoryDir(cwd), "topics");
324
325
  const out: { category: string; text: string }[] = [];
325
326
  if (!existsSync(topicsDir)) return out;
326
327
  try {
@@ -17,6 +17,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
17
  import { Type } from "typebox";
18
18
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
19
19
  import { join } from "node:path";
20
+ import { getProjectMemoryDir } from "./utils.js";
20
21
  import {
21
22
  loadScores,
22
23
  entryHash,
@@ -365,7 +366,7 @@ function registerMemoryRemove(pi: ExtensionAPI): void {
365
366
  }
366
367
 
367
368
  const topicName = CATEGORY_TO_TOPIC[category as keyof typeof CATEGORY_TO_TOPIC];
368
- const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
369
+ const topicPath = join(getProjectMemoryDir(cwd), "topics", `${topicName}.md`);
369
370
 
370
371
  if (!existsSync(topicPath)) {
371
372
  return { content: [{ type: "text", text: `Topic file not found for category "${category}".` }] };
@@ -517,7 +518,7 @@ function registerMemoryEdit(pi: ExtensionAPI): void {
517
518
  }
518
519
 
519
520
  const topicName = CATEGORY_TO_TOPIC[category as keyof typeof CATEGORY_TO_TOPIC];
520
- const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
521
+ const topicPath = join(getProjectMemoryDir(cwd), "topics", `${topicName}.md`);
521
522
 
522
523
  if (!existsSync(topicPath)) {
523
524
  return { content: [{ type: "text", text: `Topic file not found for category "${category}".` }] };
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { createLogger } from "../shared/logger.js";
14
+ import { getProjectMemoryDir } from "./utils.js";
14
15
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
15
16
  import { join } from "node:path";
16
17
  import {
@@ -58,7 +59,7 @@ export interface TopicEntry {
58
59
  // ── Topic Directory ────────────────────────────────────────────────────────
59
60
 
60
61
  function getTopicsDir(cwd: string): string {
61
- return join(cwd, ".pi", "memory", "topics");
62
+ return join(getProjectMemoryDir(cwd), "topics");
62
63
  }
63
64
 
64
65
  // ── Topic File I/O ─────────────────────────────────────────────────────────
@@ -19,6 +19,7 @@
19
19
  import { createLogger } from "../shared/logger.js";
20
20
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
21
21
  import { join } from "node:path";
22
+ import { getProjectMemoryDir } from "./utils.js";
22
23
  import {
23
24
  type MemoryCategory,
24
25
  type MemoryScope,
@@ -298,7 +299,7 @@ function supersedeEntry(cwd: string, category: string, text: string): void {
298
299
  try {
299
300
  const topicName = CATEGORY_TO_TOPIC[category as MemoryCategory];
300
301
  if (!topicName) return;
301
- const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
302
+ const topicPath = join(getProjectMemoryDir(cwd), "topics", `${topicName}.md`);
302
303
  if (!existsSync(topicPath)) return;
303
304
  const content = readFileSync(topicPath, "utf-8");
304
305
  const match = findTopicLine(content, category, text);
@@ -334,7 +335,7 @@ function supersedeEntry(cwd: string, category: string, text: string): void {
334
335
  function topicCapRejection(cwd: string, category: string, entryLine: string): AddMemoryResult | null {
335
336
  const topicName = CATEGORY_TO_TOPIC[category as MemoryCategory];
336
337
  if (!topicName) return null;
337
- const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
338
+ const topicPath = join(getProjectMemoryDir(cwd), "topics", `${topicName}.md`);
338
339
  let content = "";
339
340
  if (existsSync(topicPath)) {
340
341
  content = readFileSync(topicPath, "utf-8");
@@ -600,7 +601,7 @@ export async function addMemoryEntry(cwd: string, input: AddMemoryInput): Promis
600
601
  // 5. Topic cap + write (cap precheck already ran in the override path;
601
602
  // this re-check is a race safety net — another writer may have appended)
602
603
  const topicName = CATEGORY_TO_TOPIC[category as MemoryCategory];
603
- const topicsDir = join(cwd, ".pi", "memory", "topics");
604
+ const topicsDir = join(getProjectMemoryDir(cwd), "topics");
604
605
  const topicPath = join(topicsDir, `${topicName}.md`);
605
606
 
606
607
  if (!existsSync(topicsDir)) {
@@ -683,7 +684,7 @@ export async function addMemoryEntry(cwd: string, input: AddMemoryInput): Promis
683
684
  * entries cannot outlive their scope (issue #915).
684
685
  */
685
686
  export function pruneExpiredEntries(cwd: string): number {
686
- const topicsDir = join(cwd, ".pi", "memory", "topics");
687
+ const topicsDir = join(getProjectMemoryDir(cwd), "topics");
687
688
  if (!existsSync(topicsDir)) return 0;
688
689
  const scores = loadScores(cwd);
689
690
  const now = Date.now();
@@ -10,12 +10,12 @@
10
10
  * On first access, migrates any legacy pi-config-review-state.json automatically.
11
11
  */
12
12
 
13
- import { readFileSync, writeFileSync, unlinkSync } from "node:fs";
13
+ import { readFileSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
14
14
  import { join } from "node:path";
15
15
  import { getSetting } from "./project-settings.js";
16
16
  import { createCachedStore } from "./state-jsonl.js";
17
17
  import type { JsonlStateStore } from "./state-jsonl.js";
18
- import { resolveWorktreeRoot, getProjectDataDir } from "./utils.js";
18
+ import { resolveWorktreeRoot } from "./utils.js";
19
19
 
20
20
  type StateTransitionCallback = (state: ReviewState) => void;
21
21
  let onTransitionCb: StateTransitionCallback | null = null;
@@ -48,13 +48,21 @@ export interface ReviewState {
48
48
  const STATE_FILE_JSONL = "pi-config-review-state.jsonl";
49
49
  const LEGACY_STATE_FILE = "pi-config-review-state.json";
50
50
 
51
+ /** Per-worktree data dir — deliberately NOT the shared getProjectDataDir:
52
+ * review state must stay per-worktree (one review loop per issue branch),
53
+ * so it joins the worktree root directly (issue #918 kept this per-cwd). */
54
+ function getWorktreeDataDir(cwd: string): string {
55
+ const dir = join(resolveWorktreeRoot(cwd), ".pi", "data");
56
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
57
+ return dir;
58
+ }
59
+
51
60
  function getStore(cwd: string): JsonlStateStore<ReviewState> {
52
- const dataDir = getProjectDataDir(resolveWorktreeRoot(cwd));
53
- return createCachedStore<ReviewState>(dataDir, STATE_FILE_JSONL, LEGACY_STATE_FILE);
61
+ return createCachedStore<ReviewState>(getWorktreeDataDir(cwd), STATE_FILE_JSONL, LEGACY_STATE_FILE);
54
62
  }
55
63
 
56
64
  export function statePath(cwd: string): string {
57
- return join(getProjectDataDir(resolveWorktreeRoot(cwd)), STATE_FILE_JSONL);
65
+ return join(getWorktreeDataDir(cwd), STATE_FILE_JSONL);
58
66
  }
59
67
 
60
68
  function defaultState(): ReviewState {
@@ -108,7 +116,7 @@ function acquireLock(cwd: string): boolean {
108
116
  const maxRetries = 100;
109
117
  for (let i = 0; i < maxRetries; i++) {
110
118
  try {
111
- getProjectDataDir(resolveWorktreeRoot(cwd));
119
+ getWorktreeDataDir(cwd);
112
120
  writeFileSync(lock, String(process.pid), { flag: "wx" });
113
121
  return true;
114
122
  } catch {
@@ -100,6 +100,8 @@ interface ProjectSettings {
100
100
  pidiff_stale_lock_timeout_ms?: number;
101
101
  /** Pidiff: daemon startup timeout in seconds. */
102
102
  pidiff_daemon_startup_timeout_s?: number;
103
+ /** Distill addressed review findings into memory lessons after `reviews address`. Default: true. */
104
+ review_learning?: boolean;
103
105
  }
104
106
 
105
107
  /** Settings metadata is loaded from settings-keys.json by the cycle-free settings source. */
@@ -150,6 +152,7 @@ const PROJECT_SETTINGS_KEYS: (keyof ProjectSettings)[] = [
150
152
  "coms_probe_timeout_ms",
151
153
  "pidiff_stale_lock_timeout_ms",
152
154
  "pidiff_daemon_startup_timeout_s",
155
+ "review_learning",
153
156
  ];
154
157
  for (const key of PROJECT_SETTINGS_KEYS) {
155
158
  if (!(key in SETTINGS_KEYS)) {
@@ -482,6 +485,7 @@ export function getSetting(cwd: string, key: "coms_entry_grace_period_ms"): numb
482
485
  export function getSetting(cwd: string, key: "coms_probe_timeout_ms"): number;
483
486
  export function getSetting(cwd: string, key: "pidiff_stale_lock_timeout_ms"): number;
484
487
  export function getSetting(cwd: string, key: "pidiff_daemon_startup_timeout_s"): number;
488
+ export function getSetting(cwd: string, key: "review_learning"): boolean;
485
489
  export function getSetting(cwd: string, key: string): boolean | string | number | string[] | Record<string, { provider?: string | null; model?: string | null }> {
486
490
  const settings = getSettings(cwd);
487
491
  const def = SETTINGS_KEYS[key];
@@ -8,6 +8,7 @@
8
8
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { createHash } from "node:crypto";
11
+ import { getProjectMemoryDir } from "./utils.js";
11
12
 
12
13
  export type PromotionDestination =
13
14
  | "memory"
@@ -45,7 +46,7 @@ const DESTINATIONS: PromotionDestination[] = [
45
46
  const STATUSES: PromotionStatus[] = ["proposed", "applied", "rejected"];
46
47
 
47
48
  export function getPromotionsPath(cwd: string): string {
48
- return join(cwd, ".pi", "memory", "promotions.md");
49
+ return join(getProjectMemoryDir(cwd), "promotions.md");
49
50
  }
50
51
 
51
52
  /** Stable id from destination + category + text (dedup key). */