pi-orchestrator-config 4.1.5

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.
Files changed (175) hide show
  1. package/README.md +808 -0
  2. package/agents/api-documenter.md +32 -0
  3. package/agents/bash-expert.md +53 -0
  4. package/agents/code-reviewer-docs.md +145 -0
  5. package/agents/code-reviewer-guidelines.md +97 -0
  6. package/agents/code-reviewer-quality.md +142 -0
  7. package/agents/code-reviewer-security.md +107 -0
  8. package/agents/code-reviewer-spec.md +164 -0
  9. package/agents/debugger.md +63 -0
  10. package/agents/docker-expert.md +40 -0
  11. package/agents/docs-fetcher.md +48 -0
  12. package/agents/git-expert.md +63 -0
  13. package/agents/github-expert.md +83 -0
  14. package/agents/go-expert.md +62 -0
  15. package/agents/issue-reviewer-feasibility.md +60 -0
  16. package/agents/issue-reviewer-scope.md +63 -0
  17. package/agents/issue-reviewer-spec.md +77 -0
  18. package/agents/java-expert.md +31 -0
  19. package/agents/jenkins-expert.md +38 -0
  20. package/agents/kubernetes-expert.md +40 -0
  21. package/agents/planner.md +42 -0
  22. package/agents/python-expert.md +61 -0
  23. package/agents/reviewer.md +26 -0
  24. package/agents/scout.md +40 -0
  25. package/agents/security-auditor.md +254 -0
  26. package/agents/technical-documentation-writer.md +37 -0
  27. package/agents/test-automator.md +55 -0
  28. package/agents/test-runner.md +47 -0
  29. package/agents/ts-expert.md +23 -0
  30. package/agents/worker.md +30 -0
  31. package/extensions/acpx-provider/index.ts +104 -0
  32. package/extensions/acpx-provider/load-runtime.ts +99 -0
  33. package/extensions/acpx-provider/runtime-models.ts +40 -0
  34. package/extensions/cli-provider/agents/claude.ts +128 -0
  35. package/extensions/cli-provider/agents/cursor.ts +84 -0
  36. package/extensions/cli-provider/agents/gemini.ts +100 -0
  37. package/extensions/cli-provider/discover.ts +77 -0
  38. package/extensions/cli-provider/index.ts +57 -0
  39. package/extensions/cli-provider/parsers.ts +304 -0
  40. package/extensions/cli-provider/providers.ts +58 -0
  41. package/extensions/cli-provider/runner.ts +225 -0
  42. package/extensions/cli-provider/runtime-models.ts +32 -0
  43. package/extensions/cli-provider/session-reaper.ts +122 -0
  44. package/extensions/cli-provider/sessions.ts +533 -0
  45. package/extensions/cli-provider/shared/discover-cache.ts +235 -0
  46. package/extensions/cli-provider/types.ts +48 -0
  47. package/extensions/coms/coms-net-server.ts +1642 -0
  48. package/extensions/coms/coms-net-wrapper.ts +521 -0
  49. package/extensions/coms/coms-net.ts +1589 -0
  50. package/extensions/coms/coms-p2p.ts +1879 -0
  51. package/extensions/coms/coms-shared.ts +510 -0
  52. package/extensions/coms/coms-wrapper.ts +138 -0
  53. package/extensions/coms/index.ts +20 -0
  54. package/extensions/coms/themeMap.ts +147 -0
  55. package/extensions/image-gen/image-gen.ts +332 -0
  56. package/extensions/image-gen/index.ts +14 -0
  57. package/extensions/orchestrator/agents.ts +134 -0
  58. package/extensions/orchestrator/ask-user.ts +293 -0
  59. package/extensions/orchestrator/async-agents.ts +1037 -0
  60. package/extensions/orchestrator/async-capability.ts +129 -0
  61. package/extensions/orchestrator/async-runner.ts +324 -0
  62. package/extensions/orchestrator/async-status-parse.ts +31 -0
  63. package/extensions/orchestrator/async-status-ui.ts +201 -0
  64. package/extensions/orchestrator/async-wait.ts +36 -0
  65. package/extensions/orchestrator/btw.ts +210 -0
  66. package/extensions/orchestrator/cron-status-format.ts +126 -0
  67. package/extensions/orchestrator/cron-status-ui.ts +132 -0
  68. package/extensions/orchestrator/cron.ts +730 -0
  69. package/extensions/orchestrator/dreaming.ts +355 -0
  70. package/extensions/orchestrator/enforcement-helpers.ts +530 -0
  71. package/extensions/orchestrator/enforcement-rules.ts +329 -0
  72. package/extensions/orchestrator/enforcement.ts +828 -0
  73. package/extensions/orchestrator/extended-autocomplete.ts +593 -0
  74. package/extensions/orchestrator/git-helpers.ts +420 -0
  75. package/extensions/orchestrator/github-autocomplete.ts +250 -0
  76. package/extensions/orchestrator/icons.ts +17 -0
  77. package/extensions/orchestrator/index.ts +100 -0
  78. package/extensions/orchestrator/memory-embeddings.ts +281 -0
  79. package/extensions/orchestrator/memory-promotion.ts +449 -0
  80. package/extensions/orchestrator/memory-provenance.ts +80 -0
  81. package/extensions/orchestrator/memory-query-class.ts +90 -0
  82. package/extensions/orchestrator/memory-scoring.ts +434 -0
  83. package/extensions/orchestrator/memory-tools.ts +883 -0
  84. package/extensions/orchestrator/memory-tree.ts +212 -0
  85. package/extensions/orchestrator/nvim.ts +169 -0
  86. package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
  87. package/extensions/orchestrator/overlay-dashboard.ts +512 -0
  88. package/extensions/orchestrator/pi-config-review-state.ts +303 -0
  89. package/extensions/orchestrator/preference-extractor.ts +106 -0
  90. package/extensions/orchestrator/project-settings.ts +468 -0
  91. package/extensions/orchestrator/promotion-queue.ts +311 -0
  92. package/extensions/orchestrator/resolve-agent-model.ts +32 -0
  93. package/extensions/orchestrator/review-ui.ts +204 -0
  94. package/extensions/orchestrator/rule-placeholders.ts +12 -0
  95. package/extensions/orchestrator/rules.ts +481 -0
  96. package/extensions/orchestrator/session-search.ts +243 -0
  97. package/extensions/orchestrator/session-validation.ts +374 -0
  98. package/extensions/orchestrator/situation-report.ts +370 -0
  99. package/extensions/orchestrator/status-bar.ts +31 -0
  100. package/extensions/orchestrator/status-line.ts +192 -0
  101. package/extensions/orchestrator/status.ts +110 -0
  102. package/extensions/orchestrator/subagent-tool.ts +1748 -0
  103. package/extensions/orchestrator/task-lifecycle.ts +88 -0
  104. package/extensions/orchestrator/utils.ts +227 -0
  105. package/extensions/pidash/index.ts +15 -0
  106. package/extensions/pidash/pidash.ts +941 -0
  107. package/extensions/pidiff/index.ts +13 -0
  108. package/extensions/pidiff/pidiff.ts +369 -0
  109. package/extensions/providers/acpx-driver.ts +479 -0
  110. package/extensions/providers/built-in-drivers.ts +52 -0
  111. package/extensions/providers/claude-driver.ts +319 -0
  112. package/extensions/providers/cursor-acpx-driver.ts +398 -0
  113. package/extensions/providers/cursor-cli-driver.ts +484 -0
  114. package/extensions/providers/gemini-driver.ts +302 -0
  115. package/extensions/providers/index.ts +513 -0
  116. package/extensions/shared/build-system-prompt.ts +57 -0
  117. package/extensions/shared/create-runtime-provider.ts +190 -0
  118. package/extensions/shared/daemon-manager.ts +248 -0
  119. package/extensions/shared/file-logger.ts +115 -0
  120. package/extensions/shared/managed-refresh.ts +211 -0
  121. package/extensions/shared/provider-driver.ts +295 -0
  122. package/extensions/shared/provider-errors.ts +305 -0
  123. package/extensions/shared/provider-registry.ts +388 -0
  124. package/extensions/shared/stream-builder.ts +275 -0
  125. package/extensions/shared/types.ts +20 -0
  126. package/extensions/shared/ui/README.md +10 -0
  127. package/extensions/shared/ui/badge.tsx +52 -0
  128. package/extensions/shared/ui/button.tsx +58 -0
  129. package/extensions/shared/ui/collapsible.tsx +21 -0
  130. package/extensions/shared/ui/dropdown-menu.tsx +266 -0
  131. package/extensions/shared/ui/input.tsx +20 -0
  132. package/extensions/shared/ui/popover.tsx +114 -0
  133. package/extensions/shared/ui/resizable.tsx +48 -0
  134. package/extensions/shared/ui/scroll-area.tsx +53 -0
  135. package/extensions/shared/ui/select.tsx +201 -0
  136. package/extensions/shared/ui/separator.tsx +25 -0
  137. package/extensions/shared/ui/tooltip.tsx +64 -0
  138. package/extensions/shared/ws-client.ts +74 -0
  139. package/package.json +59 -0
  140. package/prompts/coderabbit-rate-limit.md +95 -0
  141. package/prompts/create-coms-feature-manager.md +186 -0
  142. package/prompts/create-skill.md +128 -0
  143. package/prompts/domain-model.md +109 -0
  144. package/prompts/external-ai.md +665 -0
  145. package/prompts/implement-and-review.md +25 -0
  146. package/prompts/implement.md +26 -0
  147. package/prompts/issue-review.md +256 -0
  148. package/prompts/pr-review.md +753 -0
  149. package/prompts/query-db.md +154 -0
  150. package/prompts/refine-review.md +208 -0
  151. package/prompts/release.md +235 -0
  152. package/prompts/remember.md +33 -0
  153. package/prompts/review-handler-status.md +27 -0
  154. package/prompts/review-handler.md +677 -0
  155. package/prompts/review-local.md +159 -0
  156. package/prompts/scout-and-plan.md +21 -0
  157. package/rules/00-orchestrator-core.md +42 -0
  158. package/rules/05-issue-first-workflow.md +119 -0
  159. package/rules/10-agent-routing.md +58 -0
  160. package/rules/15-mcp-launchpad.md +46 -0
  161. package/rules/20-code-review-loop.md +234 -0
  162. package/rules/25-documentation-updates.md +15 -0
  163. package/rules/30-prompt-templates.md +19 -0
  164. package/rules/35-memory.md +240 -0
  165. package/rules/40-critical-rules.md +124 -0
  166. package/rules/45-file-preview.md +29 -0
  167. package/rules/50-agent-bug-reporting.md +90 -0
  168. package/rules/55-coms-protocol.md +63 -0
  169. package/rules/60-task-tracking.md +66 -0
  170. package/skills/conflict-resolver/SKILL.md +213 -0
  171. package/skills/skill-creator/SKILL.md +85 -0
  172. package/skills/systematic-debugging/SKILL.md +353 -0
  173. package/skills/test-driven-development/SKILL.md +335 -0
  174. package/skills/update-agents-md/SKILL.md +282 -0
  175. package/templates/coms-feature-manager-prompt.md +313 -0
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Memory promotion — graduate high-evidence memories into structure.
3
+ *
4
+ * Safe auto-apply: enforcement metadata on existing memories.
5
+ * Propose-only: project_rule (never writes rules files).
6
+ * Skill: queue + dream may auto-create under .pi/skills/.
7
+ */
8
+
9
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import {
12
+ type EnforcementAction,
13
+ type EnforcementTrigger,
14
+ type MemoryCategory,
15
+ type ScoredEntry,
16
+ entryHash,
17
+ loadScores,
18
+ saveScores,
19
+ } from "./memory-scoring.js";
20
+ import { CATEGORY_TO_TOPIC, readAllTopicEntries } from "./memory-tree.js";
21
+ import {
22
+ type PromotionCandidate,
23
+ type PromotionCandidatePatch,
24
+ appendPromotions,
25
+ formatPromotionsForReport,
26
+ loadPromotions,
27
+ promotionId,
28
+ updatePromotionCandidates,
29
+ updatePromotionStatus,
30
+ } from "./promotion-queue.js";
31
+
32
+ export const EVIDENCE_ENFORCEMENT = 3;
33
+ export const EVIDENCE_SKILL = 3;
34
+ export const EVIDENCE_PROJECT_RULE = 5;
35
+
36
+ export interface InferredEnforcement {
37
+ trigger: EnforcementTrigger;
38
+ action: EnforcementAction;
39
+ actionCommand?: string;
40
+ verifier?: string;
41
+ confidence: "high" | "low";
42
+ }
43
+
44
+ /** Detect unambiguous mechanical enforcement from memory text. */
45
+ export function inferMechanicalEnforcement(text: string): InferredEnforcement | null {
46
+ const t = text.trim();
47
+
48
+ // never/don't use|run `cmd` or "cmd"
49
+ const neverCmd = t.match(
50
+ /\b(?:never|don'?t|do not)\s+(?:use|run|execute)\s+[`"']([^`"']+)[`"']/i,
51
+ );
52
+ if (neverCmd?.[1]) {
53
+ return {
54
+ trigger: `bash_contains ${neverCmd[1].trim()}` as EnforcementTrigger,
55
+ action: "block",
56
+ confidence: "high",
57
+ };
58
+ }
59
+
60
+ // never/don't git add .
61
+ const neverGitAdd = t.match(/\b(?:never|don'?t|do not)\s+.*\bgit\s+add\s+\./i);
62
+ if (neverGitAdd) {
63
+ return {
64
+ trigger: "bash_contains git add ." as EnforcementTrigger,
65
+ action: "block",
66
+ confidence: "high",
67
+ };
68
+ }
69
+
70
+ // never use tool X / never call X
71
+ const neverTool = t.match(
72
+ /\b(?:never|don'?t|do not)\s+(?:use|call|invoke)\s+(?:the\s+)?([a-z_][a-z0-9_-]{1,40})\b/i,
73
+ );
74
+ if (neverTool?.[1]) {
75
+ const tool = neverTool[1].toLowerCase();
76
+ // Avoid matching generic words
77
+ if (!["this", "that", "the", "a", "an", "any", "our"].includes(tool)) {
78
+ return {
79
+ trigger: `tool_name ${tool}` as EnforcementTrigger,
80
+ action: "block",
81
+ confidence: "high",
82
+ };
83
+ }
84
+ }
85
+
86
+ // always ask_user before <command>
87
+ const askBefore = t.match(
88
+ /\balways\s+(?:call\s+)?ask_user\s+before\s+(.+)$/i,
89
+ );
90
+ if (askBefore?.[1]) {
91
+ const cmd = askBefore[1].replace(/[.!]+$/, "").trim();
92
+ if (cmd.length > 2 && cmd.length < 80) {
93
+ return {
94
+ trigger: "tool_name bash" as EnforcementTrigger,
95
+ action: "warn",
96
+ verifier: `tool_called ask_user before ${cmd}`,
97
+ confidence: "high",
98
+ };
99
+ }
100
+ }
101
+
102
+ // warn when modifying <pattern>
103
+ // Only high-confidence when the pattern is matchable by enforcement runtime:
104
+ // `*.ext` suffix or a plain path fragment (no glob metacharacters).
105
+ const warnFile = t.match(
106
+ /\bwarn\s+when\s+modifying\s+([^\s,]+)/i,
107
+ );
108
+ if (warnFile?.[1]) {
109
+ const pattern = warnFile[1].trim();
110
+ const isExtGlob = /^\*\.[A-Za-z0-9]+$/.test(pattern);
111
+ const hasGlobMeta = /[*?[\]{}]|\*\*/.test(pattern);
112
+ if (isExtGlob || !hasGlobMeta) {
113
+ return {
114
+ trigger: `file_modified ${pattern}` as EnforcementTrigger,
115
+ action: "warn",
116
+ confidence: "high",
117
+ };
118
+ }
119
+ // Glob-like patterns (e.g. src/**/*.py) are not matchable — leave for propose-only
120
+ return null;
121
+ }
122
+
123
+ // Soft mechanical cue without extractable trigger
124
+ if (
125
+ /\b(?:never|always|don'?t|do not)\b/i.test(t) &&
126
+ /\b(?:git|bash|npm|uv|docker|kubectl|gh|curl)\b/i.test(t)
127
+ ) {
128
+ return null; // mechanical-looking but ambiguous — caller may queue low-confidence
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ function looksProcedural(text: string): boolean {
135
+ return (
136
+ /\b(step\s*\d|then\s+|after that|workflow|pipeline)\b/i.test(text) ||
137
+ (text.includes(" → ") || text.includes("->"))
138
+ );
139
+ }
140
+
141
+ function looksProjectConvention(text: string): boolean {
142
+ return /\b(always|never|must|convention|in this (repo|project)|project-wide)\b/i.test(
143
+ text,
144
+ );
145
+ }
146
+
147
+ function hasEnforcement(entry: ScoredEntry): boolean {
148
+ return !!((entry.trigger && entry.action) || entry.verifier);
149
+ }
150
+
151
+ /** Mark *(enforced)* on the topic line for an entry. */
152
+ export function markTopicEntryEnforced(
153
+ cwd: string,
154
+ category: MemoryCategory,
155
+ text: string,
156
+ ): boolean {
157
+ const topicName = CATEGORY_TO_TOPIC[category];
158
+ if (!topicName) return false;
159
+ const topicPath = join(cwd, ".pi", "memory", "topics", `${topicName}.md`);
160
+ if (!existsSync(topicPath)) return false;
161
+
162
+ let content = readFileSync(topicPath, "utf-8");
163
+ const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
164
+ const lineRe = new RegExp(
165
+ `^(- \\[${category}\\] ${escaped})((?: \\*\\(pinned\\)\\*)?)((?: \\*\\(enforced\\)\\*)?)(\\s*)$`,
166
+ "m",
167
+ );
168
+ const m = content.match(lineRe);
169
+ if (!m) return false;
170
+ if (m[3]) return true; // already enforced
171
+
172
+ content = content.replace(lineRe, `$1$2 *(enforced)*$4`);
173
+ writeFileSync(topicPath, content, "utf-8");
174
+ return true;
175
+ }
176
+
177
+ export interface TopicScored {
178
+ text: string;
179
+ category: MemoryCategory;
180
+ pinned: boolean;
181
+ hash: string;
182
+ scored: ScoredEntry;
183
+ }
184
+
185
+ function loadTopicScored(cwd: string): TopicScored[] {
186
+ const scores = loadScores(cwd);
187
+ const out: TopicScored[] = [];
188
+ for (const te of readAllTopicEntries(cwd)) {
189
+ const hash = entryHash(`- [${te.category}] ${te.text}`);
190
+ const scored = scores.entries[hash];
191
+ if (!scored) continue;
192
+ out.push({
193
+ text: te.text,
194
+ category: te.category,
195
+ pinned: te.pinned,
196
+ hash,
197
+ scored,
198
+ });
199
+ }
200
+ return out;
201
+ }
202
+
203
+ /**
204
+ * Scan memories for promotion candidates (does not write).
205
+ */
206
+ export function scanPromotionCandidates(cwd: string): PromotionCandidate[] {
207
+ const now = new Date().toISOString();
208
+ const candidates: PromotionCandidate[] = [];
209
+ const entries = loadTopicScored(cwd);
210
+
211
+ for (const e of entries) {
212
+ const evidence = e.scored.evidenceCount;
213
+ const inferred = inferMechanicalEnforcement(e.text);
214
+
215
+ // Enforcement path
216
+ if (
217
+ evidence >= EVIDENCE_ENFORCEMENT &&
218
+ !hasEnforcement(e.scored) &&
219
+ (e.category === "lesson" ||
220
+ e.category === "mistake" ||
221
+ e.category === "preference")
222
+ ) {
223
+ if (inferred && inferred.confidence === "high") {
224
+ candidates.push({
225
+ id: promotionId("enforcement", e.category, e.text),
226
+ destination: "enforcement",
227
+ text: e.text,
228
+ category: e.category,
229
+ reason: `evidenceCount=${evidence} with extractable mechanical trigger`,
230
+ evidenceCount: evidence,
231
+ status: "proposed",
232
+ trigger: inferred.trigger,
233
+ action: inferred.actionCommand
234
+ ? `run_after ${inferred.actionCommand}`
235
+ : inferred.action,
236
+ verifier: inferred.verifier,
237
+ createdAt: now,
238
+ });
239
+ } else if (
240
+ /\b(?:never|always|don'?t|do not)\b/i.test(e.text) &&
241
+ !inferred
242
+ ) {
243
+ candidates.push({
244
+ id: promotionId("enforcement", e.category, e.text),
245
+ destination: "enforcement",
246
+ text: e.text,
247
+ category: e.category,
248
+ reason: `evidenceCount=${evidence}; mechanical language but trigger needs human/LLM fill-in`,
249
+ evidenceCount: evidence,
250
+ status: "proposed",
251
+ createdAt: now,
252
+ });
253
+ }
254
+ }
255
+
256
+ // Skill path
257
+ if (
258
+ evidence >= EVIDENCE_SKILL &&
259
+ looksProcedural(e.text) &&
260
+ (e.category === "pattern" || e.category === "lesson" || e.category === "done")
261
+ ) {
262
+ const skillName = e.text
263
+ .toLowerCase()
264
+ .replace(/[^a-z0-9]+/g, "-")
265
+ .replace(/^-|-$/g, "")
266
+ .slice(0, 40) || "workflow";
267
+ candidates.push({
268
+ id: promotionId("skill", e.category, e.text),
269
+ destination: "skill",
270
+ text: e.text,
271
+ category: e.category,
272
+ reason: `evidenceCount=${evidence}; looks like a multi-step workflow`,
273
+ evidenceCount: evidence,
274
+ status: "proposed",
275
+ skillName,
276
+ skillCreated: false,
277
+ createdAt: now,
278
+ });
279
+ }
280
+
281
+ // Project rule path (propose only)
282
+ if (
283
+ evidence >= EVIDENCE_PROJECT_RULE &&
284
+ looksProjectConvention(e.text) &&
285
+ !hasEnforcement(e.scored) &&
286
+ !inferred
287
+ ) {
288
+ candidates.push({
289
+ id: promotionId("project_rule", e.category, e.text),
290
+ destination: "project_rule",
291
+ text: e.text,
292
+ category: e.category,
293
+ reason: `evidenceCount=${evidence}; project-wide convention — propose only, never auto-write rules/`,
294
+ evidenceCount: evidence,
295
+ status: "proposed",
296
+ createdAt: now,
297
+ });
298
+ }
299
+ }
300
+
301
+ return candidates;
302
+ }
303
+
304
+ export interface ApplySafeResult {
305
+ applied: number;
306
+ queued: number;
307
+ details: string[];
308
+ }
309
+
310
+ /**
311
+ * Apply high-confidence enforcement promotions; queue the rest.
312
+ * Never writes rules files.
313
+ */
314
+ export function applySafePromotions(cwd: string): ApplySafeResult {
315
+ const details: string[] = [];
316
+ let applied = 0;
317
+
318
+ const scanned = scanPromotionCandidates(cwd);
319
+ const toQueue: PromotionCandidate[] = [];
320
+ const scores = loadScores(cwd);
321
+
322
+ for (const c of scanned) {
323
+ if (c.destination !== "enforcement" || !c.trigger || !c.action) {
324
+ toQueue.push(c);
325
+ continue;
326
+ }
327
+
328
+ // Only auto-apply when action is block/warn (not run_after — safer)
329
+ if (c.action !== "block" && c.action !== "warn") {
330
+ toQueue.push(c);
331
+ continue;
332
+ }
333
+
334
+ const hash = entryHash(`- [${c.category}] ${c.text}`);
335
+ const entry = scores.entries[hash];
336
+ if (!entry || hasEnforcement(entry)) {
337
+ toQueue.push({ ...c, status: "proposed" });
338
+ continue;
339
+ }
340
+
341
+ entry.trigger = c.trigger as EnforcementTrigger;
342
+ entry.action = c.action as EnforcementAction;
343
+ if (c.verifier) entry.verifier = c.verifier;
344
+ entry.lifecycle = "active";
345
+ markTopicEntryEnforced(cwd, c.category as MemoryCategory, c.text);
346
+
347
+ const appliedCandidate: PromotionCandidate = { ...c, status: "applied" };
348
+ toQueue.push(appliedCandidate);
349
+ applied += 1;
350
+ details.push(`enforced: [${c.category}] ${c.text}`);
351
+ }
352
+
353
+ saveScores(cwd, scores);
354
+
355
+ // Append-only / batched status updates — avoid lossy full rewrite and per-item I/O.
356
+ const existing = loadPromotions(cwd);
357
+ const byId = new Map(existing.map((x) => [x.id, x]));
358
+ const toAppend: PromotionCandidate[] = [];
359
+ const patches: { id: string; patch: PromotionCandidatePatch }[] = [];
360
+
361
+ for (const c of toQueue) {
362
+ const prev = byId.get(c.id);
363
+ if (!prev) {
364
+ toAppend.push(c);
365
+ byId.set(c.id, c);
366
+ continue;
367
+ }
368
+ // Don't reopen applied/rejected as proposed
369
+ if (prev.status !== "proposed" && c.status === "proposed") continue;
370
+ if (c.status === "applied" && prev.status === "proposed") {
371
+ // Merge enforcement metadata for auditability; keep human reason + createdAt
372
+ const merged: PromotionCandidate = {
373
+ ...prev,
374
+ ...c,
375
+ status: "applied",
376
+ reason: prev.reason || c.reason,
377
+ createdAt: prev.createdAt,
378
+ // Prefer new scan values when present; otherwise keep queued metadata
379
+ evidenceCount: c.evidenceCount ?? prev.evidenceCount,
380
+ trigger: c.trigger ?? prev.trigger,
381
+ action: c.action ?? prev.action,
382
+ verifier: c.verifier ?? prev.verifier,
383
+ skillName: c.skillName ?? prev.skillName,
384
+ skillCreated: c.skillCreated ?? prev.skillCreated,
385
+ };
386
+ patches.push({
387
+ id: c.id,
388
+ patch: {
389
+ status: "applied",
390
+ reason: merged.reason,
391
+ ...(c.evidenceCount !== undefined
392
+ ? { evidenceCount: c.evidenceCount }
393
+ : {}),
394
+ ...(c.trigger !== undefined ? { trigger: c.trigger } : {}),
395
+ ...(c.action !== undefined ? { action: c.action } : {}),
396
+ ...(c.verifier !== undefined ? { verifier: c.verifier } : {}),
397
+ ...(c.skillName !== undefined ? { skillName: c.skillName } : {}),
398
+ ...(c.skillCreated !== undefined
399
+ ? { skillCreated: c.skillCreated }
400
+ : {}),
401
+ },
402
+ });
403
+ byId.set(c.id, merged);
404
+ }
405
+ }
406
+ if (patches.length > 0) updatePromotionCandidates(cwd, patches);
407
+ if (toAppend.length > 0) appendPromotions(cwd, toAppend);
408
+
409
+ const queued = [...byId.values()].filter((c) => c.status === "proposed").length;
410
+ return { applied, queued, details };
411
+ }
412
+
413
+ /**
414
+ * Full promotion pass: scan, apply safe enforcement, refresh queue.
415
+ * Call after reinforce threshold crossings and after dream completes.
416
+ */
417
+ export function runPromotionPass(cwd: string): ApplySafeResult {
418
+ try {
419
+ return applySafePromotions(cwd);
420
+ } catch (e: any) {
421
+ console.debug("[memory-promotion] pass failed:", e?.message || e);
422
+ return { applied: 0, queued: 0, details: [] };
423
+ }
424
+ }
425
+
426
+ /**
427
+ * After reinforce: if evidence crossed a threshold, run promotion pass.
428
+ */
429
+ export function maybePromoteAfterReinforce(
430
+ cwd: string,
431
+ entryLine: string,
432
+ ): ApplySafeResult | null {
433
+ const scores = loadScores(cwd);
434
+ const hash = entryHash(entryLine);
435
+ const entry = scores.entries[hash];
436
+ if (!entry) return null;
437
+
438
+ const crossed =
439
+ entry.evidenceCount === EVIDENCE_ENFORCEMENT ||
440
+ entry.evidenceCount === EVIDENCE_SKILL ||
441
+ entry.evidenceCount === EVIDENCE_PROJECT_RULE ||
442
+ (entry.evidenceCount > EVIDENCE_ENFORCEMENT &&
443
+ entry.evidenceCount % 3 === 0);
444
+
445
+ if (!crossed) return null;
446
+ return runPromotionPass(cwd);
447
+ }
448
+
449
+ export { formatPromotionsForReport, updatePromotionStatus, appendPromotions };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Provenance pending merge — dream writes a sidecar JSON; onComplete merges
3
+ * into memory-scores.json without asking the LLM to edit scores JSON.
4
+ *
5
+ * Sidecar: `.pi/memory/provenance-pending.json`
6
+ * Shape: { entries: [{ category, text, sourceSession?, derivedFrom?, informs? }] }
7
+ */
8
+
9
+ import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { entryHash, loadScores, saveScores, type MemoryCategory } from "./memory-scoring.js";
12
+
13
+ export interface ProvenancePendingItem {
14
+ category: MemoryCategory | string;
15
+ text: string;
16
+ sourceSession?: string;
17
+ derivedFrom?: string;
18
+ informs?: string[];
19
+ }
20
+
21
+ export interface ProvenancePendingFile {
22
+ entries: ProvenancePendingItem[];
23
+ }
24
+
25
+ export function getProvenancePendingPath(cwd: string): string {
26
+ return join(cwd, ".pi", "memory", "provenance-pending.json");
27
+ }
28
+
29
+ export function writeProvenancePending(
30
+ cwd: string,
31
+ entries: ProvenancePendingItem[],
32
+ ): void {
33
+ const path = getProvenancePendingPath(cwd);
34
+ const dir = dirname(path);
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ const payload: ProvenancePendingFile = { entries };
37
+ writeFileSync(path, JSON.stringify(payload, null, 2) + "\n", "utf-8");
38
+ }
39
+
40
+ /**
41
+ * Merge provenance-pending.json into scores, then delete the sidecar.
42
+ * Returns number of entries updated.
43
+ */
44
+ export function mergeProvenancePending(cwd: string): number {
45
+ const path = getProvenancePendingPath(cwd);
46
+ if (!existsSync(path)) return 0;
47
+
48
+ let pending: ProvenancePendingFile;
49
+ try {
50
+ pending = JSON.parse(readFileSync(path, "utf-8")) as ProvenancePendingFile;
51
+ } catch (e: any) {
52
+ console.debug("[memory-provenance] parse failed:", e?.message || e);
53
+ return 0;
54
+ }
55
+
56
+ if (!pending?.entries?.length) {
57
+ try { unlinkSync(path); } catch { /* ignore */ }
58
+ return 0;
59
+ }
60
+
61
+ const scores = loadScores(cwd);
62
+ let updated = 0;
63
+
64
+ for (const item of pending.entries) {
65
+ if (!item?.category || !item?.text) continue;
66
+ const hash = entryHash(`- [${item.category}] ${item.text}`);
67
+ const entry = scores.entries[hash];
68
+ if (!entry) continue;
69
+
70
+ if (item.sourceSession) entry.sourceSession = item.sourceSession;
71
+ if (item.derivedFrom) entry.derivedFrom = item.derivedFrom;
72
+ if (item.informs?.length) entry.informs = item.informs;
73
+ updated += 1;
74
+ }
75
+
76
+ if (updated > 0) saveScores(cwd, scores);
77
+
78
+ try { unlinkSync(path); } catch { /* ignore */ }
79
+ return updated;
80
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Query-class classifier for memory injection routing.
3
+ * Cheap heuristics only — no LLM call.
4
+ */
5
+
6
+ export type QueryClass = "pr_review" | "git_release" | "debug" | "general";
7
+
8
+ export interface QueryClassBias {
9
+ /** Categories to boost (higher section budget / earlier priority) */
10
+ boostCategories: string[];
11
+ /** Vector topK override */
12
+ vectorTopK: number;
13
+ /** Extra note injected into situation report when relevant */
14
+ hint?: string;
15
+ }
16
+
17
+ const CLASS_BIAS: Record<QueryClass, QueryClassBias> = {
18
+ pr_review: {
19
+ boostCategories: ["mistake", "pattern", "lesson"],
20
+ vectorTopK: 7,
21
+ hint: "PR review context — prefer mistakes/patterns; check `.pi/data/review-guidelines.md` when reviewing.",
22
+ },
23
+ git_release: {
24
+ boostCategories: ["lesson", "decision", "preference"],
25
+ vectorTopK: 6,
26
+ hint: "Git/release context — prefer enforced lessons and decisions.",
27
+ },
28
+ debug: {
29
+ boostCategories: ["mistake", "lesson"],
30
+ vectorTopK: 7,
31
+ hint: "Debug context — prefer mistakes and lessons.",
32
+ },
33
+ general: {
34
+ boostCategories: [],
35
+ vectorTopK: 5,
36
+ },
37
+ };
38
+
39
+ const PR_REVIEW_RE =
40
+ /\b(pr review|code review|review (this |the )?(pr|pull request)|gh pr\b|pull request)\b/i;
41
+ const GIT_RELEASE_RE =
42
+ /\b(git (commit|push|tag|rebase|merge)|release|changelog|semver|deploy(ment)?)\b/i;
43
+ const DEBUG_RE =
44
+ /\b(error|stack( ?trace)?|traceback|fail(ed|ing|ure)?|exception|bug|debug|segfault|panic)\b/i;
45
+
46
+ /**
47
+ * Classify the user prompt into a query class for injection bias.
48
+ */
49
+ export function classifyQueryClass(prompt: string): QueryClass {
50
+ const text = (prompt || "").trim();
51
+ if (!text) return "general";
52
+
53
+ if (PR_REVIEW_RE.test(text)) return "pr_review";
54
+ if (GIT_RELEASE_RE.test(text)) return "git_release";
55
+ if (DEBUG_RE.test(text)) return "debug";
56
+ return "general";
57
+ }
58
+
59
+ export function getQueryClassBias(queryClass: QueryClass): QueryClassBias {
60
+ return CLASS_BIAS[queryClass];
61
+ }
62
+
63
+ /**
64
+ * Reorder section priority: boosted categories get a modest priority lift
65
+ * (lower number = earlier). Non-boosted keep base order.
66
+ */
67
+ export function sectionPriorityBoost(
68
+ sectionName: string,
69
+ basePriority: number,
70
+ queryClass: QueryClass,
71
+ ): number {
72
+ const bias = getQueryClassBias(queryClass);
73
+ if (bias.boostCategories.length === 0) return basePriority;
74
+
75
+ const topicMap: Record<string, string> = {
76
+ "Active Preferences": "preference",
77
+ "Active Lessons": "lesson",
78
+ "Vetoes & Mistakes": "mistake",
79
+ Patterns: "pattern",
80
+ "Recent Decisions": "decision",
81
+ "Recent Completions": "done",
82
+ };
83
+ const cat = topicMap[sectionName];
84
+ if (cat && bias.boostCategories.includes(cat)) {
85
+ // Lower number sorts earlier; allow 0/negative so boosted sections
86
+ // beat unboosted priority-1 sections (e.g. preferences).
87
+ return basePriority - 3;
88
+ }
89
+ return basePriority;
90
+ }