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.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- package/templates/coms-feature-manager-prompt.md +313 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Situation Report — token-budgeted memory context for system prompts.
|
|
3
|
+
*
|
|
4
|
+
* Replaces raw memory dump with a structured, prioritized report
|
|
5
|
+
* that fits within a token budget. Higher-priority sections are always
|
|
6
|
+
* included; lower-priority sections are truncated or dropped.
|
|
7
|
+
*
|
|
8
|
+
* Architecture inspired by OpenHuman (https://github.com/tinyhumansai/openhuman).
|
|
9
|
+
* Clean-room TypeScript implementation under MIT — not a code translation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
type ScoredEntry,
|
|
16
|
+
type MemoryCategory,
|
|
17
|
+
loadScores,
|
|
18
|
+
entryHash,
|
|
19
|
+
rebuild,
|
|
20
|
+
} from "./memory-scoring.js";
|
|
21
|
+
import { listTopics, readAllTopicEntries } from "./memory-tree.js";
|
|
22
|
+
import { formatPromotionsForReport } from "./promotion-queue.js";
|
|
23
|
+
import {
|
|
24
|
+
type QueryClass,
|
|
25
|
+
getQueryClassBias,
|
|
26
|
+
sectionPriorityBoost,
|
|
27
|
+
} from "./memory-query-class.js";
|
|
28
|
+
|
|
29
|
+
// ── Constants ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Rough chars-per-token estimate for budget enforcement */
|
|
32
|
+
const CHARS_PER_TOKEN = 4;
|
|
33
|
+
|
|
34
|
+
/** Default total token budget for the situation report */
|
|
35
|
+
const DEFAULT_TOKEN_BUDGET = 1700;
|
|
36
|
+
|
|
37
|
+
/** Section definitions with priority and token budgets */
|
|
38
|
+
interface SectionDef {
|
|
39
|
+
name: string;
|
|
40
|
+
priority: number;
|
|
41
|
+
tokenBudget: number;
|
|
42
|
+
filter: (category: MemoryCategory, entry: ScoredEntry) => boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SECTIONS: SectionDef[] = [
|
|
46
|
+
{
|
|
47
|
+
name: "Active Preferences",
|
|
48
|
+
priority: 1,
|
|
49
|
+
tokenBudget: 400,
|
|
50
|
+
filter: (cat) => cat === "preference",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "Active Lessons",
|
|
54
|
+
priority: 2,
|
|
55
|
+
tokenBudget: 400,
|
|
56
|
+
filter: (cat) => cat === "lesson",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "Vetoes & Mistakes",
|
|
60
|
+
priority: 3,
|
|
61
|
+
tokenBudget: 200,
|
|
62
|
+
filter: (cat) => cat === "mistake",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "Patterns",
|
|
66
|
+
priority: 4,
|
|
67
|
+
tokenBudget: 200,
|
|
68
|
+
filter: (cat) => cat === "pattern",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: "Recent Decisions",
|
|
72
|
+
priority: 5,
|
|
73
|
+
tokenBudget: 200,
|
|
74
|
+
filter: (cat, entry) => {
|
|
75
|
+
if (cat !== "decision") return false;
|
|
76
|
+
// Only include decisions from last 7 days
|
|
77
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
78
|
+
const lastReinforced = new Date(entry.lastReinforced).getTime();
|
|
79
|
+
return Date.now() - lastReinforced < sevenDaysMs;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "Recent Completions",
|
|
84
|
+
priority: 6,
|
|
85
|
+
tokenBudget: 200,
|
|
86
|
+
filter: (cat, entry) => {
|
|
87
|
+
if (cat !== "done") return false;
|
|
88
|
+
// Only include completions from last 3 days
|
|
89
|
+
const threeDaysMs = 3 * 24 * 60 * 60 * 1000;
|
|
90
|
+
const lastReinforced = new Date(entry.lastReinforced).getTime();
|
|
91
|
+
return Date.now() - lastReinforced < threeDaysMs;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// ── Report Building ────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
interface MemoryEntryWithText {
|
|
99
|
+
text: string;
|
|
100
|
+
category: MemoryCategory;
|
|
101
|
+
scored: ScoredEntry;
|
|
102
|
+
isPinned: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run a full rebuild cycle: rescore all entries from topics (source of truth).
|
|
107
|
+
* Called on session_start, every 30 min, and after dreaming.
|
|
108
|
+
*/
|
|
109
|
+
export function rebuildAndOrganize(cwd: string): void {
|
|
110
|
+
const topicsDir = join(cwd, ".pi", "memory", "topics");
|
|
111
|
+
if (!existsSync(topicsDir)) return;
|
|
112
|
+
|
|
113
|
+
const topicEntries = readAllTopicEntries(cwd);
|
|
114
|
+
if (topicEntries.length > 0) {
|
|
115
|
+
rebuild(cwd, topicEntries);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface SituationReportOptions {
|
|
120
|
+
/** Query class for section budget / priority bias */
|
|
121
|
+
queryClass?: QueryClass;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build a situation report from scored memory entries.
|
|
126
|
+
*
|
|
127
|
+
* Returns a formatted markdown string that fits within the token budget,
|
|
128
|
+
* ready for injection into the system prompt.
|
|
129
|
+
*/
|
|
130
|
+
export function buildSituationReport(
|
|
131
|
+
cwd: string,
|
|
132
|
+
tokenBudget: number = DEFAULT_TOKEN_BUDGET,
|
|
133
|
+
options: SituationReportOptions = {},
|
|
134
|
+
): string {
|
|
135
|
+
// Exhausted memory budget — do not inject (avoids divide-by-zero / NaN usage %)
|
|
136
|
+
if (tokenBudget <= 0) return "";
|
|
137
|
+
|
|
138
|
+
const topicsDir = join(cwd, ".pi", "memory", "topics");
|
|
139
|
+
const queryClass: QueryClass = options.queryClass ?? "general";
|
|
140
|
+
const bias = getQueryClassBias(queryClass);
|
|
141
|
+
|
|
142
|
+
const hasTopics = existsSync(topicsDir);
|
|
143
|
+
|
|
144
|
+
// Read scores (rebuilt on session start + dreaming, not every turn)
|
|
145
|
+
const scores = hasTopics ? loadScores(cwd) : { entries: {}, lastRebuild: "" };
|
|
146
|
+
|
|
147
|
+
// Read from topic files (source of truth) with hotness ordering
|
|
148
|
+
const topics = hasTopics ? listTopics(cwd) : [];
|
|
149
|
+
const topicEntries = hasTopics ? readAllTopicEntries(cwd) : [];
|
|
150
|
+
|
|
151
|
+
// Build topic hotness map for section ordering
|
|
152
|
+
const topicHotness = new Map<string, number>();
|
|
153
|
+
for (const t of topics) {
|
|
154
|
+
topicHotness.set(t.name, t.hotness);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build entries from topic files (source of truth)
|
|
158
|
+
const entries: MemoryEntryWithText[] = [];
|
|
159
|
+
for (const te of topicEntries) {
|
|
160
|
+
const entryLine = `- [${te.category}] ${te.text}`;
|
|
161
|
+
const hash = entryHash(entryLine);
|
|
162
|
+
const scored = scores.entries[hash];
|
|
163
|
+
if (!scored) continue;
|
|
164
|
+
// Only include active and provisional entries
|
|
165
|
+
if (scored.lifecycle !== "active" && scored.lifecycle !== "provisional") continue;
|
|
166
|
+
entries.push({
|
|
167
|
+
text: te.text,
|
|
168
|
+
category: te.category,
|
|
169
|
+
scored,
|
|
170
|
+
isPinned: te.pinned || scored.userState === "pinned",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build body sections first, then compute actual capacity from emitted content
|
|
175
|
+
let charBudget = Math.max(0, tokenBudget * CHARS_PER_TOKEN);
|
|
176
|
+
const bodySections: string[] = [];
|
|
177
|
+
|
|
178
|
+
// Reserve budget for header + possible warning + ground truth instruction
|
|
179
|
+
const headerReserve = 330;
|
|
180
|
+
charBudget = Math.max(0, charBudget - headerReserve);
|
|
181
|
+
|
|
182
|
+
// Promotion candidates (low budget) — show even when topics empty
|
|
183
|
+
const promoSection = formatPromotionsForReport(cwd, 5);
|
|
184
|
+
if (promoSection) {
|
|
185
|
+
const promoBudget = Math.min(150 * CHARS_PER_TOKEN, charBudget);
|
|
186
|
+
const truncated =
|
|
187
|
+
promoSection.length > promoBudget
|
|
188
|
+
? promoSection.slice(0, promoBudget) + "\n"
|
|
189
|
+
: promoSection;
|
|
190
|
+
bodySections.push(truncated);
|
|
191
|
+
charBudget -= truncated.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (bias.hint && queryClass !== "general") {
|
|
195
|
+
const hintLine = `> Query class: \`${queryClass}\` — ${bias.hint}\n`;
|
|
196
|
+
bodySections.push(hintLine);
|
|
197
|
+
charBudget -= hintLine.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (entries.length === 0 && bodySections.length === 0) return "";
|
|
201
|
+
|
|
202
|
+
// Sort entries by score descending within each category
|
|
203
|
+
entries.sort((a, b) => b.scored.score - a.scored.score);
|
|
204
|
+
|
|
205
|
+
// Pinned entries always come first (no budget limit)
|
|
206
|
+
const pinned = entries.filter((e) => e.isPinned);
|
|
207
|
+
if (pinned.length > 0) {
|
|
208
|
+
const pinnedSection = formatSection("Pinned", pinned);
|
|
209
|
+
bodySections.push(pinnedSection);
|
|
210
|
+
charBudget -= pinnedSection.length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Build each scored section in priority order, boosted by topic hotness + query class
|
|
214
|
+
const nonPinned = entries.filter((e) => !e.isPinned);
|
|
215
|
+
|
|
216
|
+
// Sort sections: use base priority but boost sections whose topic is hotter
|
|
217
|
+
const sortedSections = [...SECTIONS].sort((a, b) => {
|
|
218
|
+
// Map section names to topic names
|
|
219
|
+
const topicMap: Record<string, string> = {
|
|
220
|
+
"Active Preferences": "preferences",
|
|
221
|
+
"Active Lessons": "lessons",
|
|
222
|
+
"Vetoes & Mistakes": "mistakes",
|
|
223
|
+
"Patterns": "patterns",
|
|
224
|
+
"Recent Decisions": "decisions",
|
|
225
|
+
"Recent Completions": "completions",
|
|
226
|
+
};
|
|
227
|
+
const aPriority = sectionPriorityBoost(a.name, a.priority, queryClass);
|
|
228
|
+
const bPriority = sectionPriorityBoost(b.name, b.priority, queryClass);
|
|
229
|
+
const aHot = topicHotness.get(topicMap[a.name] || "") || 0;
|
|
230
|
+
const bHot = topicHotness.get(topicMap[b.name] || "") || 0;
|
|
231
|
+
// Primary: priority (lower = higher). Secondary: hotness (higher = first)
|
|
232
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
233
|
+
return bHot - aHot;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
for (const sectionDef of sortedSections) {
|
|
237
|
+
if (charBudget <= 0) break;
|
|
238
|
+
|
|
239
|
+
const sectionEntries = nonPinned.filter((e) =>
|
|
240
|
+
sectionDef.filter(e.category, e.scored)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (sectionEntries.length === 0) continue;
|
|
244
|
+
|
|
245
|
+
// Boost token budget for query-class-relevant sections
|
|
246
|
+
const sectionCatMap: Record<string, MemoryCategory> = {
|
|
247
|
+
"Active Preferences": "preference",
|
|
248
|
+
"Active Lessons": "lesson",
|
|
249
|
+
"Vetoes & Mistakes": "mistake",
|
|
250
|
+
Patterns: "pattern",
|
|
251
|
+
"Recent Decisions": "decision",
|
|
252
|
+
"Recent Completions": "done",
|
|
253
|
+
};
|
|
254
|
+
const sectionCat = sectionCatMap[sectionDef.name];
|
|
255
|
+
const boosted = !!(sectionCat && bias.boostCategories.includes(sectionCat));
|
|
256
|
+
const sectionTokenBudget = boosted
|
|
257
|
+
? Math.floor(sectionDef.tokenBudget * 1.25)
|
|
258
|
+
: sectionDef.tokenBudget;
|
|
259
|
+
|
|
260
|
+
const sectionCharBudget = Math.min(
|
|
261
|
+
sectionTokenBudget * CHARS_PER_TOKEN,
|
|
262
|
+
charBudget,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const section = formatSectionWithBudget(
|
|
266
|
+
sectionDef.name,
|
|
267
|
+
sectionEntries,
|
|
268
|
+
sectionCharBudget,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (section) {
|
|
272
|
+
bodySections.push(section);
|
|
273
|
+
charBudget -= section.length;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Build header and optional warning, then include their sizes in the capacity calculation
|
|
278
|
+
const bodyContent = bodySections.join("\n");
|
|
279
|
+
|
|
280
|
+
// Build header placeholder to measure its approximate length
|
|
281
|
+
// (actual % will be computed after including header+warning)
|
|
282
|
+
const headerTemplate = `# Project Memory [XX% — XXXX/${tokenBudget} tokens]\n`;
|
|
283
|
+
const warningText = "> ⚠️ Memory above 80% capacity. Before adding new entries, consolidate or remove existing ones using memory_remove.\n";
|
|
284
|
+
|
|
285
|
+
// Ground Truth instruction — always included
|
|
286
|
+
const groundTruthLine =
|
|
287
|
+
"> **Ground Truth:** Memories above and contextually relevant memories injected below are authoritative. " +
|
|
288
|
+
"Use them directly — do not re-discover or re-verify information already in your context window.\n";
|
|
289
|
+
|
|
290
|
+
// Estimate: include header; include warning if body alone suggests >80%
|
|
291
|
+
const bodyChars = bodyContent.length;
|
|
292
|
+
const roughUsage = (bodyChars + headerTemplate.length + groundTruthLine.length) / CHARS_PER_TOKEN / tokenBudget;
|
|
293
|
+
const includeWarning = roughUsage >= 0.8;
|
|
294
|
+
|
|
295
|
+
// Include header + warning + ground truth in the actual usage
|
|
296
|
+
const headerChars = headerTemplate.length + groundTruthLine.length + (includeWarning ? warningText.length : 0);
|
|
297
|
+
const actualCharsUsed = bodyChars + headerChars;
|
|
298
|
+
const totalTokensUsed = Math.floor(actualCharsUsed / CHARS_PER_TOKEN);
|
|
299
|
+
const usagePercent = Math.floor((totalTokensUsed / tokenBudget) * 100);
|
|
300
|
+
|
|
301
|
+
// Build final report: header first, then body
|
|
302
|
+
const reportSections: string[] = [];
|
|
303
|
+
const header = `# Project Memory [${usagePercent}% — ${totalTokensUsed}/${tokenBudget} tokens]\n`;
|
|
304
|
+
reportSections.push(header);
|
|
305
|
+
|
|
306
|
+
// Consolidation warning when above 80%
|
|
307
|
+
if (usagePercent >= 80) {
|
|
308
|
+
reportSections.push(warningText);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Ground Truth instruction (text defined above for budget accounting)
|
|
312
|
+
reportSections.push(groundTruthLine);
|
|
313
|
+
|
|
314
|
+
reportSections.push(...bodySections);
|
|
315
|
+
|
|
316
|
+
return reportSections.join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Approximate total token budget for the system prompt (rules + context + memory). */
|
|
320
|
+
const SYSTEM_PROMPT_TOKEN_BUDGET = 8000;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Estimate available token budget for memory based on system prompt size.
|
|
324
|
+
* Shrinks memory budget when the system prompt is already large.
|
|
325
|
+
*/
|
|
326
|
+
export function estimateMemoryBudget(systemPromptLength: number, totalBudget: number = SYSTEM_PROMPT_TOKEN_BUDGET): number {
|
|
327
|
+
// Memory competes with rules, context files, skills, etc.
|
|
328
|
+
const systemPromptTokens = Math.ceil(systemPromptLength / CHARS_PER_TOKEN);
|
|
329
|
+
const remaining = totalBudget - systemPromptTokens;
|
|
330
|
+
// No budget left — skip memory injection entirely
|
|
331
|
+
if (remaining <= 0) return 0;
|
|
332
|
+
// Clamp to DEFAULT_TOKEN_BUDGET maximum
|
|
333
|
+
return Math.min(DEFAULT_TOKEN_BUDGET, remaining);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
function formatSection(name: string, entries: MemoryEntryWithText[]): string {
|
|
339
|
+
const header = `## ${name}\n`;
|
|
340
|
+
const lines = entries.map((e) => `- [${e.category}] ${e.text}`);
|
|
341
|
+
return header + lines.join("\n") + "\n";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function formatSectionWithBudget(
|
|
345
|
+
name: string,
|
|
346
|
+
entries: MemoryEntryWithText[],
|
|
347
|
+
charBudget: number,
|
|
348
|
+
): string | null {
|
|
349
|
+
const header = `## ${name}\n`;
|
|
350
|
+
if (charBudget < header.length + 20) return null;
|
|
351
|
+
|
|
352
|
+
let remaining = charBudget - header.length;
|
|
353
|
+
const lines: string[] = [];
|
|
354
|
+
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
const line = `- [${entry.category}] ${entry.text}`;
|
|
357
|
+
if (remaining < line.length + 1) break; // +1 for newline
|
|
358
|
+
lines.push(line);
|
|
359
|
+
remaining -= line.length + 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (lines.length === 0) return null;
|
|
363
|
+
|
|
364
|
+
const omitted = entries.length - lines.length;
|
|
365
|
+
if (omitted > 0) {
|
|
366
|
+
lines.push(`- ... ${omitted} more (lower priority, omitted for context budget)`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return header + lines.join("\n") + "\n";
|
|
370
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status bar slot registry — single source of truth for layout and ordering.
|
|
3
|
+
*
|
|
4
|
+
* All status bar updates go through setSlot/clearSlot. No module should
|
|
5
|
+
* call ctx.ui.setStatus directly — use this module instead.
|
|
6
|
+
*
|
|
7
|
+
* Adding a new slot: add to SLOTS, import setSlot in the consumer.
|
|
8
|
+
* Reordering: change the sort-key prefix here — nothing else changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Slot sort-keys — alphabetical order determines left-to-right position in the bar. */
|
|
12
|
+
const SLOTS = {
|
|
13
|
+
time: "1-time",
|
|
14
|
+
async: "2-async",
|
|
15
|
+
crons: "3-crons",
|
|
16
|
+
dream: "3b-dream",
|
|
17
|
+
git: "4-git",
|
|
18
|
+
review: "5-review",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
export type StatusBarSlot = keyof typeof SLOTS;
|
|
22
|
+
|
|
23
|
+
/** Set a status bar slot value. Pass undefined to clear. */
|
|
24
|
+
export function setSlot(slot: StatusBarSlot, text: string | undefined, ctx: any): void {
|
|
25
|
+
ctx?.ui?.setStatus(SLOTS[slot], text);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Clear a status bar slot. */
|
|
29
|
+
export function clearSlot(slot: StatusBarSlot, ctx: any): void {
|
|
30
|
+
ctx?.ui?.setStatus(SLOTS[slot], undefined);
|
|
31
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git status line, container indicator, desktop notifications, git poller.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { hyperlink } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
getCurrentBranch,
|
|
9
|
+
getOpenPr,
|
|
10
|
+
isGithubRepo,
|
|
11
|
+
runGit,
|
|
12
|
+
scheduleOpenPrStatusRefresh,
|
|
13
|
+
} from "./git-helpers.js";
|
|
14
|
+
import { ICON_SEP, ICON_CONTAINER, ICON_GIT_CLEAN, ICON_GIT_DIRTY } from "./icons.js";
|
|
15
|
+
import { setSlot } from "./status-bar.js";
|
|
16
|
+
import { clockHHMM } from "./utils.js";
|
|
17
|
+
|
|
18
|
+
export function registerStatusLine(
|
|
19
|
+
pi: ExtensionAPI,
|
|
20
|
+
IN_CONTAINER: boolean,
|
|
21
|
+
terminalNotify: (title: string, body: string) => void,
|
|
22
|
+
): void {
|
|
23
|
+
// ── Combined status line builder ───────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
let lastStatusText = "";
|
|
26
|
+
const buildStatus = (ctx: any, gitPart: string) => {
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (IN_CONTAINER) parts.push(ICON_CONTAINER);
|
|
30
|
+
parts.push(gitPart);
|
|
31
|
+
|
|
32
|
+
const text = parts.join(ctx.ui.theme.fg("dim", ICON_SEP));
|
|
33
|
+
if (text === lastStatusText) return; // Skip redundant re-renders
|
|
34
|
+
lastStatusText = text;
|
|
35
|
+
setSlot("git", text, ctx);
|
|
36
|
+
// Clear legacy status keys to avoid duplicates
|
|
37
|
+
ctx.ui.setStatus("container", undefined);
|
|
38
|
+
ctx.ui.setStatus("git", undefined);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Git branch status line ─────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
let lastCtx: any = null;
|
|
44
|
+
let lastBranch: string | null = null;
|
|
45
|
+
|
|
46
|
+
const updateBranch = (_event: any, ctx: any) => {
|
|
47
|
+
lastCtx = ctx;
|
|
48
|
+
try {
|
|
49
|
+
const b = getCurrentBranch(ctx.cwd);
|
|
50
|
+
if (!b) {
|
|
51
|
+
lastBranch = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
lastBranch = b;
|
|
55
|
+
|
|
56
|
+
const status = runGit(["status", "--porcelain"], ctx.cwd);
|
|
57
|
+
let modified = 0,
|
|
58
|
+
added = 0,
|
|
59
|
+
deleted = 0,
|
|
60
|
+
untracked = 0;
|
|
61
|
+
if (status.code === 0 && status.stdout) {
|
|
62
|
+
for (const line of status.stdout.split("\n")) {
|
|
63
|
+
if (!line.trim()) continue;
|
|
64
|
+
const xy = line.slice(0, 2);
|
|
65
|
+
if (xy.includes("?")) untracked++;
|
|
66
|
+
else if (xy.includes("D")) deleted++;
|
|
67
|
+
else if (xy.includes("A")) added++;
|
|
68
|
+
else if (xy.includes("M") || xy.includes("R") || xy.includes("C"))
|
|
69
|
+
modified++;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const changes: string[] = [];
|
|
74
|
+
if (modified > 0)
|
|
75
|
+
changes.push(ctx.ui.theme.fg("warning", `~${modified}`));
|
|
76
|
+
if (added > 0) changes.push(ctx.ui.theme.fg("success", `+${added}`));
|
|
77
|
+
if (deleted > 0) changes.push(ctx.ui.theme.fg("error", `-${deleted}`));
|
|
78
|
+
if (untracked > 0) changes.push(ctx.ui.theme.fg("dim", `?${untracked}`));
|
|
79
|
+
const icon =
|
|
80
|
+
changes.length > 0
|
|
81
|
+
? ctx.ui.theme.fg("error", ICON_GIT_DIRTY)
|
|
82
|
+
: ctx.ui.theme.fg("success", ICON_GIT_CLEAN);
|
|
83
|
+
let gitPart =
|
|
84
|
+
changes.length > 0 ? `${icon} ${changes.join(" ")}` : icon;
|
|
85
|
+
|
|
86
|
+
// Sync cache only — never block the status path on `gh`.
|
|
87
|
+
// One isGithubRepo check for getOpenPr + refresh schedule.
|
|
88
|
+
const isGh = isGithubRepo(ctx.cwd);
|
|
89
|
+
const pr = isGh ? getOpenPr(ctx.cwd, b, { assumeGithub: true }) : null;
|
|
90
|
+
if (pr) {
|
|
91
|
+
const prLabel = ctx.ui.theme.fg(
|
|
92
|
+
"accent",
|
|
93
|
+
hyperlink(`#${pr.number}`, pr.url),
|
|
94
|
+
);
|
|
95
|
+
gitPart = `${gitPart} ${prLabel}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
buildStatus(ctx, gitPart);
|
|
99
|
+
|
|
100
|
+
// Per-key coalesce lives in refreshOpenPr — no global pending gate.
|
|
101
|
+
if (isGh) {
|
|
102
|
+
scheduleOpenPrStatusRefresh({
|
|
103
|
+
cwd: ctx.cwd,
|
|
104
|
+
branch: b,
|
|
105
|
+
shownPr: pr,
|
|
106
|
+
getState: () => ({ lastCtx, lastBranch }),
|
|
107
|
+
onRerender: (c) => updateBranch(null, c),
|
|
108
|
+
assumeGithub: true,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (e: any) { console.debug("[status-line] git status update failed:", e?.message || e); }
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
pi.on("session_start", updateBranch);
|
|
115
|
+
pi.on("agent_end", updateBranch);
|
|
116
|
+
pi.on("turn_end", updateBranch);
|
|
117
|
+
pi.on("tool_result", updateBranch);
|
|
118
|
+
pi.on("tool_execution_end", updateBranch);
|
|
119
|
+
|
|
120
|
+
// Poll git status every 5s for updates during long-running operations
|
|
121
|
+
pi.on("session_start", (_event, ctx) => {
|
|
122
|
+
lastCtx = ctx;
|
|
123
|
+
});
|
|
124
|
+
const gitPoller = setInterval(() => {
|
|
125
|
+
if (!lastCtx) return;
|
|
126
|
+
try {
|
|
127
|
+
lastCtx.ui.theme; // probe for stale ctx
|
|
128
|
+
updateBranch(null, lastCtx);
|
|
129
|
+
} catch (e: any) {
|
|
130
|
+
console.debug("[status-line] git poller error, pausing until next session_start:", e?.message || e);
|
|
131
|
+
lastCtx = null;
|
|
132
|
+
}
|
|
133
|
+
}, 5000);
|
|
134
|
+
if (gitPoller.unref) gitPoller.unref();
|
|
135
|
+
|
|
136
|
+
// ── Last-activity timestamp ────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
let lastActivityTime: Date | null = null;
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
const ago = (since: Date): string => {
|
|
143
|
+
const diffMs = Date.now() - since.getTime();
|
|
144
|
+
if (diffMs < 0) return "now";
|
|
145
|
+
const mins = Math.floor(diffMs / 60_000);
|
|
146
|
+
if (mins < 1) return "now";
|
|
147
|
+
if (mins < 60) return `${mins}m`;
|
|
148
|
+
const hours = Math.floor(mins / 60);
|
|
149
|
+
if (hours < 24) return `${hours}h`;
|
|
150
|
+
return `${Math.floor(hours / 24)}d`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const updateTimestamp = (ctx: any) => {
|
|
154
|
+
if (!lastActivityTime) return;
|
|
155
|
+
const text = ctx.ui.theme.fg("dim", `⏱ ${clockHHMM(lastActivityTime)} (${ago(lastActivityTime)})`);
|
|
156
|
+
setSlot("time", text, ctx);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const touchActivity = (_event: any, ctx: any) => {
|
|
160
|
+
lastActivityTime = new Date();
|
|
161
|
+
updateTimestamp(ctx);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
pi.on("session_start", touchActivity);
|
|
165
|
+
pi.on("turn_end", touchActivity);
|
|
166
|
+
pi.on("agent_end", touchActivity);
|
|
167
|
+
pi.on("tool_execution_end", touchActivity);
|
|
168
|
+
|
|
169
|
+
const timePoller = setInterval(() => {
|
|
170
|
+
if (!lastCtx || !lastActivityTime) return;
|
|
171
|
+
try {
|
|
172
|
+
lastCtx.ui.theme; // probe for stale ctx
|
|
173
|
+
updateTimestamp(lastCtx);
|
|
174
|
+
} catch (e: any) {
|
|
175
|
+
console.debug("[status-line] time poller error, pausing until next session_start:", e?.message || e);
|
|
176
|
+
lastCtx = null;
|
|
177
|
+
}
|
|
178
|
+
}, 30_000);
|
|
179
|
+
if (timePoller.unref) timePoller.unref();
|
|
180
|
+
|
|
181
|
+
pi.on("session_shutdown", (_event) => {
|
|
182
|
+
clearInterval(gitPoller);
|
|
183
|
+
clearInterval(timePoller);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── Desktop notifications — notify when pi is truly idle ─────────────
|
|
187
|
+
|
|
188
|
+
pi.on("agent_settled", async () => {
|
|
189
|
+
terminalNotify("pi", "Task completed");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
}
|