@artale/pi-pai 4.4.1 → 4.6.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.
- package/.gitlab-ci.yml +41 -0
- package/AGENTS.md +32 -0
- package/INSTALL.md +224 -0
- package/README.md +192 -138
- package/SYSTEM.md +120 -0
- package/SYSTEM.md.pai +120 -120
- package/VERSION +1 -0
- package/dist/extension.d.ts +32 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +1118 -0
- package/dist/extension.js.map +1 -0
- package/dist/lifeos-pai-core.d.ts +15 -0
- package/dist/lifeos-pai-core.d.ts.map +1 -0
- package/dist/lifeos-pai-core.js +290 -0
- package/dist/lifeos-pai-core.js.map +1 -0
- package/memory/learning/.gitkeep +0 -0
- package/memory/state/.gitkeep +0 -0
- package/memory/work/.gitkeep +0 -0
- package/models.json +43 -0
- package/package.json +21 -9
- package/settings.json +14 -0
- package/src/lifeos-pai-core.ts +608 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* π-PAI v4.2 — Personal AI Infrastructure Extension for Pi
|
|
3
|
+
*
|
|
4
|
+
* Synced with Miessler's PAI v4.0.3 algorithm:
|
|
5
|
+
* - Algorithm: OBSERVE → PLAN → DECIDE → EXECUTE → VERIFY (v4 loop)
|
|
6
|
+
* - ISC decomposition: splitting test, count gates, anti-criteria
|
|
7
|
+
* - 5 effort levels with ISC minimums: Standard(8)/Extended(16)/Advanced(24)/Deep(40)/Comprehensive(64)
|
|
8
|
+
* - Time budgets per effort level with auto-compress at 150%
|
|
9
|
+
* - Capability selection + invocation tracking
|
|
10
|
+
* - Ratings + sentiment tracking with trend analysis
|
|
11
|
+
* - Agent persona dispatch (architect, pentester, designer, etc.)
|
|
12
|
+
* - Plans directory convention (.pi/plans/)
|
|
13
|
+
* - Self-evolution trigger (learning pattern detection)
|
|
14
|
+
* - Enhanced observability: PaiSplittingTest, PaiIscGate, PaiCapability, PaiEffortCompress events
|
|
15
|
+
*
|
|
16
|
+
* Also includes:
|
|
17
|
+
* - Ralph Wiggum deterministic iteration engine
|
|
18
|
+
* - Damage control (YAML-based path/command guards)
|
|
19
|
+
* - Templates (trading, saas, devops, research, agent)
|
|
20
|
+
*
|
|
21
|
+
* v4.2: Full v4.0.3 sync — 7 features:
|
|
22
|
+
* 1. ISC splitting test (atomicity validation)
|
|
23
|
+
* 2. ISC count gate (effort-level minimums)
|
|
24
|
+
* 3. Anti-criteria (/pai isca)
|
|
25
|
+
* 4. Capability selection (/pai capabilities)
|
|
26
|
+
* 5. Capability invocation tracking (tool_call counting)
|
|
27
|
+
* 6. Time budgets with auto-compress warning
|
|
28
|
+
* 7. Enhanced observability events for dashboard
|
|
29
|
+
*/
|
|
30
|
+
import { Type } from '@sinclair/typebox';
|
|
31
|
+
import * as fs from 'fs';
|
|
32
|
+
import * as path from 'path';
|
|
33
|
+
import * as os from 'os';
|
|
34
|
+
import YAML from 'js-yaml';
|
|
35
|
+
// ── Observability Bridge ─────────────────────────────────────────────────────
|
|
36
|
+
// Writes events to ~/.claude/history/raw-outputs/ in the same format as
|
|
37
|
+
// Claude Code's universal_hook_logger.py — so the PAI dashboard at :5172
|
|
38
|
+
// shows Pi sessions alongside Claude Code sessions.
|
|
39
|
+
let observeSessionId = `pi-pai-${Date.now().toString(36)}`;
|
|
40
|
+
function emitObserveEvent(hookType, payload) {
|
|
41
|
+
try {
|
|
42
|
+
const now = new Date();
|
|
43
|
+
const dir = path.join(os.homedir(), '.claude', 'history', 'raw-outputs', `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`);
|
|
44
|
+
if (!fs.existsSync(dir))
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
const file = path.join(dir, `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}_all-events.jsonl`);
|
|
47
|
+
const entry = {
|
|
48
|
+
source_app: 'pi-pai',
|
|
49
|
+
session_id: observeSessionId,
|
|
50
|
+
hook_event_type: hookType,
|
|
51
|
+
payload: { session_id: observeSessionId, hook_event_name: hookType, ...payload },
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
55
|
+
}
|
|
56
|
+
catch { /* non-blocking */ }
|
|
57
|
+
}
|
|
58
|
+
// ── v4.0.3: ISC Count Minimums & Min Capabilities (Feature #2, #4) ───────────
|
|
59
|
+
const ISC_MINIMUMS = {
|
|
60
|
+
standard: 8, extended: 16, advanced: 24, deep: 40, comprehensive: 64,
|
|
61
|
+
};
|
|
62
|
+
// ── v4.0.3: Time Budgets in minutes (Feature #6) ────────────────────────────
|
|
63
|
+
const TIME_BUDGETS_MIN = {
|
|
64
|
+
standard: 2, extended: 8, advanced: 16, deep: 32, comprehensive: 120,
|
|
65
|
+
};
|
|
66
|
+
const TIME_COMPRESS_FACTOR = 1.5; // auto-compress warning at 150% of budget
|
|
67
|
+
// ── v4.0.3: ISC Splitting Test (Feature #1) ─────────────────────────────────
|
|
68
|
+
// Returns warnings for non-atomic ISC criteria
|
|
69
|
+
function splittingTest(criterion) {
|
|
70
|
+
const warnings = [];
|
|
71
|
+
// "And" / "With" test
|
|
72
|
+
if (/\b(and|with|including|plus)\b/i.test(criterion)) {
|
|
73
|
+
warnings.push(`Contains "${criterion.match(/\b(and|with|including|plus)\b/i)?.[0]}" — likely two criteria. Split them.`);
|
|
74
|
+
}
|
|
75
|
+
// Scope word test
|
|
76
|
+
if (/\b(all|every|complete|full|each)\b/i.test(criterion)) {
|
|
77
|
+
warnings.push(`Contains "${criterion.match(/\b(all|every|complete|full|each)\b/i)?.[0]}" — enumerate what this means specifically.`);
|
|
78
|
+
}
|
|
79
|
+
// Length test — atomic ISC should be 8-12 words
|
|
80
|
+
const words = criterion.trim().split(/\s+/).length;
|
|
81
|
+
if (words > 15)
|
|
82
|
+
warnings.push(`${words} words — too long for atomic ISC (target 8-12). Split or simplify.`);
|
|
83
|
+
if (words < 4)
|
|
84
|
+
warnings.push(`${words} words — too vague. Be specific and testable.`);
|
|
85
|
+
// Domain boundary test
|
|
86
|
+
const domains = [/\bUI\b|display|render|visible|button|page/i, /\bAPI\b|endpoint|request|response|status/i, /\bdata|database|schema|field|column/i, /\blogic|flow|condition|branch|validate/i];
|
|
87
|
+
const crossedDomains = domains.filter(d => d.test(criterion));
|
|
88
|
+
if (crossedDomains.length > 1)
|
|
89
|
+
warnings.push('Crosses multiple domains (UI/API/data/logic) — split per domain boundary.');
|
|
90
|
+
return warnings;
|
|
91
|
+
}
|
|
92
|
+
const AGENT_PERSONAS = {
|
|
93
|
+
architect: {
|
|
94
|
+
name: 'Architect',
|
|
95
|
+
role: 'System design and architecture decisions',
|
|
96
|
+
systemPrompt: 'You are a senior system architect. Focus on: scalability, separation of concerns, API design, data flow, and failure modes. Propose 2-3 options with tradeoffs. Be opinionated — recommend the best option. Reference established patterns (hexagonal, event-driven, CQRS) when relevant.',
|
|
97
|
+
},
|
|
98
|
+
engineer: {
|
|
99
|
+
name: 'Engineer',
|
|
100
|
+
role: 'Implementation and code quality',
|
|
101
|
+
systemPrompt: 'You are a senior software engineer. Write production-quality code: proper error handling, types, tests, and documentation. Follow the codebase conventions. No shortcuts — if something needs a test, write the test.',
|
|
102
|
+
},
|
|
103
|
+
pentester: {
|
|
104
|
+
name: 'Pentester',
|
|
105
|
+
role: 'Security review and vulnerability assessment',
|
|
106
|
+
systemPrompt: 'You are a penetration tester and security researcher. Review code for: injection attacks (SQL, command, prompt), auth bypass, SSRF, path traversal, insecure deserialization, secrets in code, and dependency vulnerabilities. Report findings with severity (Critical/High/Medium/Low), exploit scenario, and remediation.',
|
|
107
|
+
},
|
|
108
|
+
designer: {
|
|
109
|
+
name: 'Designer',
|
|
110
|
+
role: 'UX/UI design and user experience',
|
|
111
|
+
systemPrompt: 'You are a senior product designer. Focus on: information architecture, visual hierarchy, accessibility (WCAG 2.1 AA), responsive design, and interaction patterns. Propose designs with rationale. Consider edge cases: empty states, error states, loading states, overflow.',
|
|
112
|
+
},
|
|
113
|
+
reviewer: {
|
|
114
|
+
name: 'Code Reviewer',
|
|
115
|
+
role: 'Code review and quality gates',
|
|
116
|
+
systemPrompt: 'You are a meticulous code reviewer. Check: correctness, edge cases, error handling, naming, complexity, test coverage, and performance. Categorize findings as P1 (must fix), P2 (should fix), P3 (nit). Be direct — if code is wrong, say so.',
|
|
117
|
+
},
|
|
118
|
+
researcher: {
|
|
119
|
+
name: 'Researcher',
|
|
120
|
+
role: 'Deep investigation and analysis',
|
|
121
|
+
systemPrompt: 'You are a thorough researcher. Investigate topics systematically: define the question, gather evidence, analyze findings, synthesize conclusions. Cite sources. Flag uncertainty. Separate facts from opinions.',
|
|
122
|
+
},
|
|
123
|
+
qa: {
|
|
124
|
+
name: 'QA Tester',
|
|
125
|
+
role: 'Quality assurance and test planning',
|
|
126
|
+
systemPrompt: 'You are a QA engineer. Think adversarially: what can go wrong? Create test plans covering: happy paths, edge cases, error paths, boundary values, concurrency, and regression. Write concrete test cases with expected results.',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const EMPTY_RULES = { bashToolPatterns: [], zeroAccessPaths: [], readOnlyPaths: [], noDeletePaths: [] };
|
|
130
|
+
function loadDamageRules(cwd) {
|
|
131
|
+
const candidates = [
|
|
132
|
+
path.join(cwd, '.pi', 'damage-control-rules.yaml'),
|
|
133
|
+
path.join(cwd, 'damage-control-rules.yaml'),
|
|
134
|
+
path.join(__dirname, '..', 'damage-control-rules.yaml'),
|
|
135
|
+
];
|
|
136
|
+
for (const f of candidates) {
|
|
137
|
+
try {
|
|
138
|
+
if (!fs.existsSync(f))
|
|
139
|
+
continue;
|
|
140
|
+
const raw = YAML.load(fs.readFileSync(f, 'utf8'));
|
|
141
|
+
return { bashToolPatterns: raw.bashToolPatterns || [], zeroAccessPaths: raw.zeroAccessPaths || [], readOnlyPaths: raw.readOnlyPaths || [], noDeletePaths: raw.noDeletePaths || [] };
|
|
142
|
+
}
|
|
143
|
+
catch { /* skip bad files */ }
|
|
144
|
+
}
|
|
145
|
+
return EMPTY_RULES;
|
|
146
|
+
}
|
|
147
|
+
function isPathMatch(target, pattern, cwd) {
|
|
148
|
+
const expanded = pattern.startsWith('~') ? path.join(os.homedir(), pattern.slice(1)) : pattern;
|
|
149
|
+
const norm = path.normalize(expanded).replace(/\\/g, '/');
|
|
150
|
+
const abs = path.normalize(path.isAbsolute(target) ? target : path.resolve(cwd, target)).replace(/\\/g, '/');
|
|
151
|
+
if (norm.endsWith('/'))
|
|
152
|
+
return abs.startsWith(norm) || abs.startsWith(norm.slice(0, -1));
|
|
153
|
+
if (norm.includes('*')) {
|
|
154
|
+
const re = new RegExp('^' + norm.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
|
|
155
|
+
return re.test(path.basename(abs)) || re.test(abs);
|
|
156
|
+
}
|
|
157
|
+
return path.basename(abs) === norm || abs.endsWith('/' + norm);
|
|
158
|
+
}
|
|
159
|
+
function loadTemplates() {
|
|
160
|
+
const ext = path.join(__dirname, '..', 'templates.json');
|
|
161
|
+
try {
|
|
162
|
+
if (fs.existsSync(ext))
|
|
163
|
+
return JSON.parse(fs.readFileSync(ext, 'utf8'));
|
|
164
|
+
}
|
|
165
|
+
catch { /* fall through */ }
|
|
166
|
+
return {
|
|
167
|
+
trading: { mission: 'Build a profitable algorithmic trading system', goals: ['Develop and backtest core strategy', 'Achieve >55% win rate on paper trades', 'Deploy live with risk management', 'Maintain Sharpe ratio >1.5'], challenges: ['Overfitting risk on historical data', 'Execution latency in live markets'] },
|
|
168
|
+
saas: { mission: 'Launch a production SaaS product', goals: ['Ship MVP with auth, billing, and core feature', 'Acquire first 10 paying users', 'Achieve <2s p95 page load', 'Set up CI/CD and monitoring'], challenges: ['Scope creep', 'Premature optimization'] },
|
|
169
|
+
devops: { mission: 'Build reliable infrastructure and deployment pipeline', goals: ['Automate deployments with zero downtime', 'Set up monitoring and alerting', 'Achieve 99.9% uptime SLA', 'Document runbooks for on-call'], challenges: ['Alert fatigue', 'Configuration drift'] },
|
|
170
|
+
research: { mission: 'Complete deep research project with actionable findings', goals: ['Define research questions and scope', 'Collect and analyze primary sources', 'Synthesize findings into report', 'Present recommendations'], challenges: ['Source reliability', 'Scope management'] },
|
|
171
|
+
agent: { mission: 'Build and ship a production AI agent', goals: ['Define agent capabilities and constraints', 'Implement tool use and error handling', 'Test with adversarial inputs', 'Deploy with monitoring and kill switch'], challenges: ['Prompt injection risk', 'Cost control', 'Hallucination detection'] },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
// ── Sentiment Analysis (Steal #3) ────────────────────────────────────────────
|
|
175
|
+
function inferSentiment(score, context) {
|
|
176
|
+
if (score >= 7)
|
|
177
|
+
return 'positive';
|
|
178
|
+
if (score <= 3)
|
|
179
|
+
return 'negative';
|
|
180
|
+
const neg = /bad|broken|wrong|fail|slow|bug|crash|awful|terrible|worse|hate|frustrat/i;
|
|
181
|
+
const pos = /great|fast|clean|nice|perfect|love|excellent|smooth|solid/i;
|
|
182
|
+
if (neg.test(context))
|
|
183
|
+
return 'negative';
|
|
184
|
+
if (pos.test(context))
|
|
185
|
+
return 'positive';
|
|
186
|
+
return 'neutral';
|
|
187
|
+
}
|
|
188
|
+
function ratingTrend(ratings, window = 5) {
|
|
189
|
+
const avg = ratings.length ? ratings.reduce((s, r) => s + r.score, 0) / ratings.length : 0;
|
|
190
|
+
const recent = ratings.slice(-window);
|
|
191
|
+
const recentAvg = recent.length ? recent.reduce((s, r) => s + r.score, 0) / recent.length : 0;
|
|
192
|
+
const delta = recentAvg - avg;
|
|
193
|
+
return { trend: delta > 0.5 ? 'improving' : delta < -0.5 ? 'declining' : 'stable', avg: +avg.toFixed(1), recent: +recentAvg.toFixed(1) };
|
|
194
|
+
}
|
|
195
|
+
// ── v4.0.3: ISC Splitting Test (Feature #1) ─────────────────────────────────
|
|
196
|
+
// Validates that each ISC is truly atomic — no compound criteria hiding behind
|
|
197
|
+
// "and", "with", scope words, or domain boundary violations.
|
|
198
|
+
const SPLIT_CONJUNCTIONS = /\b(and|as well as|along with|in addition to|plus|also|while|whilst)\b/i;
|
|
199
|
+
const SPLIT_SCOPE_WORDS = /\b(all|every|each|any|both|multiple|various|several)\b/i;
|
|
200
|
+
const SPLIT_DOMAIN_MARKERS = /\b(frontend and backend|client and server|ui and api|read and write|create and delete|input and output)\b/i;
|
|
201
|
+
function iscSplittingTest(criterion) {
|
|
202
|
+
// Check for conjunctions (compound criteria)
|
|
203
|
+
const conjMatch = criterion.match(SPLIT_CONJUNCTIONS);
|
|
204
|
+
if (conjMatch) {
|
|
205
|
+
const parts = criterion.split(SPLIT_CONJUNCTIONS).filter(p => p.trim() && !SPLIT_CONJUNCTIONS.test(p));
|
|
206
|
+
return {
|
|
207
|
+
pass: false,
|
|
208
|
+
reason: `Compound criterion — "${conjMatch[0]}" joins multiple conditions`,
|
|
209
|
+
suggestion: parts.length >= 2
|
|
210
|
+
? `Split into:\n 1. ${parts[0].trim()}\n 2. ${parts[1].trim()}`
|
|
211
|
+
: `Remove "${conjMatch[0]}" and create separate ISCs for each condition`,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// Check for scope words (too broad)
|
|
215
|
+
const scopeMatch = criterion.match(SPLIT_SCOPE_WORDS);
|
|
216
|
+
if (scopeMatch) {
|
|
217
|
+
return {
|
|
218
|
+
pass: false,
|
|
219
|
+
reason: `Scope word "${scopeMatch[0]}" — criterion may cover multiple items`,
|
|
220
|
+
suggestion: `Be specific: which exact item(s)? Replace "${scopeMatch[0]}" with a concrete target`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Check for domain boundary violations
|
|
224
|
+
const domainMatch = criterion.match(SPLIT_DOMAIN_MARKERS);
|
|
225
|
+
if (domainMatch) {
|
|
226
|
+
return {
|
|
227
|
+
pass: false,
|
|
228
|
+
reason: `Cross-domain criterion — "${domainMatch[0]}" spans boundaries`,
|
|
229
|
+
suggestion: `Split into separate ISCs per domain`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
// Check word count (8-12 is ideal for ISC)
|
|
233
|
+
const words = criterion.trim().split(/\s+/).length;
|
|
234
|
+
if (words > 20) {
|
|
235
|
+
return { pass: false, reason: `Too long (${words} words) — likely compound`, suggestion: 'Shorten to 8-12 words or split into multiple ISCs' };
|
|
236
|
+
}
|
|
237
|
+
if (words < 4) {
|
|
238
|
+
return { pass: false, reason: `Too short (${words} words) — likely not testable`, suggestion: 'Add specifics: what exactly should be verified?' };
|
|
239
|
+
}
|
|
240
|
+
return { pass: true };
|
|
241
|
+
}
|
|
242
|
+
// ── Self-Evolution Trigger (Steal #4) ────────────────────────────────────────
|
|
243
|
+
function detectRepeatingPatterns(learnings) {
|
|
244
|
+
const counts = new Map();
|
|
245
|
+
for (const l of learnings) {
|
|
246
|
+
// Normalize: lowercase, strip punctuation, take first 6 words
|
|
247
|
+
const key = l.insight.toLowerCase().replace(/[^a-z0-9 ]/g, '').split(/\s+/).slice(0, 6).join(' ');
|
|
248
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
249
|
+
}
|
|
250
|
+
return Array.from(counts.entries()).filter(([_, c]) => c >= 3).map(([k]) => k);
|
|
251
|
+
}
|
|
252
|
+
// ── Plans Directory (Steal #5) ───────────────────────────────────────────────
|
|
253
|
+
function ensurePlansDir(cwd) {
|
|
254
|
+
const plansDir = path.join(cwd, '.pi', 'plans');
|
|
255
|
+
if (!fs.existsSync(plansDir))
|
|
256
|
+
fs.mkdirSync(plansDir, { recursive: true });
|
|
257
|
+
return plansDir;
|
|
258
|
+
}
|
|
259
|
+
function listPlans(cwd) {
|
|
260
|
+
const dir = path.join(cwd, '.pi', 'plans');
|
|
261
|
+
if (!fs.existsSync(dir))
|
|
262
|
+
return [];
|
|
263
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.md')).sort().reverse();
|
|
264
|
+
}
|
|
265
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
266
|
+
function persist(pi, key, data) {
|
|
267
|
+
pi.appendEntry(key, { ...data, ts: new Date().toISOString() });
|
|
268
|
+
}
|
|
269
|
+
// ── Extension ────────────────────────────────────────────────────────────────
|
|
270
|
+
export default function (pi) {
|
|
271
|
+
const state = {
|
|
272
|
+
mission: null, goals: new Map(), challenges: new Map(),
|
|
273
|
+
learnings: [], ratings: [], innerLoop: null,
|
|
274
|
+
iterationCount: 0, ralphIteration: 0, ralphActive: false,
|
|
275
|
+
};
|
|
276
|
+
let rules = EMPTY_RULES;
|
|
277
|
+
let widgetCtx = null;
|
|
278
|
+
// v4.0.3 algorithm: OBSERVE → PLAN → DECIDE → EXECUTE → VERIFY
|
|
279
|
+
const PHASES = ['OBSERVE', 'PLAN', 'DECIDE', 'EXECUTE', 'VERIFY'];
|
|
280
|
+
const EFFORTS = ['standard', 'extended', 'advanced', 'deep', 'comprehensive'];
|
|
281
|
+
function notify(msg, type = 'info') {
|
|
282
|
+
widgetCtx?.ui.notify(msg, type);
|
|
283
|
+
}
|
|
284
|
+
// ── Widget ─────────────────────────────────────────────────────────────
|
|
285
|
+
function updateWidget() {
|
|
286
|
+
if (!widgetCtx?.hasUI)
|
|
287
|
+
return;
|
|
288
|
+
widgetCtx.ui.setWidget('pai-status', (_tui, theme) => ({
|
|
289
|
+
render(width) {
|
|
290
|
+
const lines = [];
|
|
291
|
+
if (!state.mission) {
|
|
292
|
+
lines.push(theme.fg('dim', ' π-PAI v4: /pai mission <statement> to begin'));
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
295
|
+
const raw = state.mission ?? '';
|
|
296
|
+
const m = raw.length > width - 20 ? raw.slice(0, width - 23) + '...' : raw;
|
|
297
|
+
lines.push(theme.fg('accent', ' 🎯 ') + theme.fg('success', m));
|
|
298
|
+
const goals = Array.from(state.goals.values());
|
|
299
|
+
const a = goals.filter(g => g.status === 'active').length;
|
|
300
|
+
const b = goals.filter(g => g.status === 'blocked').length;
|
|
301
|
+
const c = goals.filter(g => g.status === 'completed').length;
|
|
302
|
+
const { trend, avg, recent } = ratingTrend(state.ratings);
|
|
303
|
+
const trendIcon = trend === 'improving' ? '📈' : trend === 'declining' ? '📉' : '➡️';
|
|
304
|
+
if (goals.length || state.ratings.length) {
|
|
305
|
+
lines.push(theme.fg('dim', ' Goals: ') + theme.fg('success', `${a}⚡`) + ' ' +
|
|
306
|
+
theme.fg('warning', `${b}🚫`) + ' ' + theme.fg('muted', `${c}✓`) +
|
|
307
|
+
theme.fg('dim', ' │ ') + theme.fg('accent', `${state.learnings.length} learnings`) +
|
|
308
|
+
theme.fg('dim', ' │ ⭐') + theme.fg('accent', `${avg}`) +
|
|
309
|
+
theme.fg('dim', ` ${trendIcon}${recent} (${state.ratings.length})`));
|
|
310
|
+
}
|
|
311
|
+
if (state.innerLoop) {
|
|
312
|
+
const idx = PHASES.indexOf(state.innerLoop.phase);
|
|
313
|
+
const bar = PHASES.map((_, i) => i < idx ? theme.fg('success', '●') : i === idx ? theme.fg('accent', '◉') : theme.fg('dim', '○')).join(' ');
|
|
314
|
+
const elapsed = Math.round((Date.now() - state.innerLoop.startTime) / 1000);
|
|
315
|
+
const min = ISC_MINIMUMS[state.innerLoop.effort];
|
|
316
|
+
const iscProgress = `${state.innerLoop.isc.length}/${min}`;
|
|
317
|
+
const budget = TIME_BUDGETS_MIN[state.innerLoop.effort];
|
|
318
|
+
const elapsedMin = Math.round(elapsed / 60);
|
|
319
|
+
const timeColor = elapsedMin > budget * TIME_COMPRESS_FACTOR ? 'error' : elapsedMin > budget ? 'warning' : 'dim';
|
|
320
|
+
lines.push(theme.fg('dim', ' Loop: ') + bar +
|
|
321
|
+
theme.fg('dim', ` [${state.innerLoop.phase}] ${state.innerLoop.effort}`) +
|
|
322
|
+
theme.fg('dim', ' │ ISC:') + theme.fg(state.innerLoop.isc.length >= min ? 'success' : 'warning', iscProgress) +
|
|
323
|
+
(state.innerLoop.iscA.length ? theme.fg('dim', ' A:') + theme.fg('accent', `${state.innerLoop.iscA.length}`) : '') +
|
|
324
|
+
theme.fg('dim', ' │ ') + theme.fg(timeColor, `${elapsedMin}/${budget}min`));
|
|
325
|
+
}
|
|
326
|
+
if (state.ralphActive)
|
|
327
|
+
lines.push(theme.fg('warning', ` 🔄 Ralph #${state.ralphIteration}`) + theme.fg('dim', ' running...'));
|
|
328
|
+
// Self-evolution warning
|
|
329
|
+
const patterns = detectRepeatingPatterns(state.learnings);
|
|
330
|
+
if (patterns.length)
|
|
331
|
+
lines.push(theme.fg('warning', ` ⚠️ ${patterns.length} repeating pattern(s) — consider /pai evolve`));
|
|
332
|
+
return lines;
|
|
333
|
+
},
|
|
334
|
+
invalidate() { },
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
// ── /pai subcommand dispatch table ─────────────────────────────────────
|
|
338
|
+
const paiCommands = {
|
|
339
|
+
mission(rest) {
|
|
340
|
+
if (!rest) {
|
|
341
|
+
notify('Usage: /pai mission <statement>', 'error');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
state.mission = rest;
|
|
345
|
+
persist(pi, 'pai-mission', { mission: rest });
|
|
346
|
+
emitObserveEvent('PaiMission', { mission: rest });
|
|
347
|
+
notify(`🎯 Mission: ${rest}`, 'info');
|
|
348
|
+
updateWidget();
|
|
349
|
+
},
|
|
350
|
+
goal(rest) {
|
|
351
|
+
if (!rest) {
|
|
352
|
+
notify('Usage: /pai goal <title>', 'error');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const id = `g${state.goals.size}`;
|
|
356
|
+
state.goals.set(id, { id, title: rest, status: 'active', priority: 'p1', isc: [] });
|
|
357
|
+
persist(pi, 'pai-goal', { id, title: rest, status: 'active' });
|
|
358
|
+
emitObserveEvent('PaiGoal', { goal_id: id, title: rest, status: 'active' });
|
|
359
|
+
notify(`✅ Goal ${id}: ${rest}`, 'info');
|
|
360
|
+
updateWidget();
|
|
361
|
+
},
|
|
362
|
+
done(rest) {
|
|
363
|
+
const goal = state.goals.get(rest.trim());
|
|
364
|
+
if (!goal) {
|
|
365
|
+
notify(`Goal "${rest.trim()}" not found`, 'error');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
goal.status = 'completed';
|
|
369
|
+
persist(pi, 'pai-goal-done', { goalId: rest.trim() });
|
|
370
|
+
notify(`🎉 Completed: ${goal.title}`, 'info');
|
|
371
|
+
updateWidget();
|
|
372
|
+
},
|
|
373
|
+
block(rest) {
|
|
374
|
+
const goal = state.goals.get(rest.trim());
|
|
375
|
+
if (!goal) {
|
|
376
|
+
notify(`Goal "${rest.trim()}" not found`, 'error');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
goal.status = 'blocked';
|
|
380
|
+
persist(pi, 'pai-goal-blocked', { goalId: rest.trim() });
|
|
381
|
+
notify(`🚫 Blocked: ${goal.title}`, 'warning');
|
|
382
|
+
updateWidget();
|
|
383
|
+
},
|
|
384
|
+
challenge(rest) {
|
|
385
|
+
if (!rest) {
|
|
386
|
+
notify('Usage: /pai challenge <description>', 'error');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const id = `c${state.challenges.size}`;
|
|
390
|
+
state.challenges.set(id, { id, title: rest, severity: 'medium', affectedGoals: [] });
|
|
391
|
+
persist(pi, 'pai-challenge', { id, title: rest });
|
|
392
|
+
notify(`⚠️ Challenge ${id}: ${rest}`, 'warning');
|
|
393
|
+
updateWidget();
|
|
394
|
+
},
|
|
395
|
+
learn(rest) {
|
|
396
|
+
if (!rest) {
|
|
397
|
+
notify('Usage: /pai learn <insight>', 'error');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const sentiment = inferSentiment(5, rest);
|
|
401
|
+
state.learnings.push({ insight: rest, confidence: 0.8, category: 'domain', timestamp: new Date(), sentiment });
|
|
402
|
+
persist(pi, 'pai-learning', { insight: rest, category: 'domain', sentiment });
|
|
403
|
+
emitObserveEvent('PaiLearning', { insight: rest, category: 'domain', sentiment });
|
|
404
|
+
notify(`📚 Learning: ${rest}`, 'info');
|
|
405
|
+
// Self-evolution trigger: check for repeating patterns
|
|
406
|
+
const patterns = detectRepeatingPatterns(state.learnings);
|
|
407
|
+
if (patterns.length)
|
|
408
|
+
notify(`⚠️ ${patterns.length} pattern(s) repeating 3+ times — run /pai evolve to address`, 'warning');
|
|
409
|
+
updateWidget();
|
|
410
|
+
},
|
|
411
|
+
loop(rest) {
|
|
412
|
+
const goal = rest || state.mission || 'unnamed';
|
|
413
|
+
state.innerLoop = { phase: 'OBSERVE', goal, effort: 'standard', isc: [], iscA: [], capabilities: new Map(), data: {}, startTime: Date.now() };
|
|
414
|
+
emitObserveEvent('PaiAlgorithmStart', { goal, phase: 'OBSERVE', effort: 'standard' });
|
|
415
|
+
notify(`🔄 Algorithm started: ${goal} [OBSERVE]`, 'info');
|
|
416
|
+
updateWidget();
|
|
417
|
+
},
|
|
418
|
+
effort(rest) {
|
|
419
|
+
if (!state.innerLoop) {
|
|
420
|
+
notify('No active loop', 'error');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const level = rest.toLowerCase();
|
|
424
|
+
if (!EFFORTS.includes(level)) {
|
|
425
|
+
notify(`Usage: /pai effort ${EFFORTS.join('|')}`, 'error');
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
state.innerLoop.effort = level;
|
|
429
|
+
notify(`⚡ Effort: ${level}`, 'info');
|
|
430
|
+
updateWidget();
|
|
431
|
+
},
|
|
432
|
+
isc(rest) {
|
|
433
|
+
if (!state.innerLoop) {
|
|
434
|
+
notify('No active loop', 'error');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!rest) {
|
|
438
|
+
notify('Usage: /pai isc <8-12 word testable criterion>', 'error');
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
// v4.0.3 Feature #1: Splitting test
|
|
442
|
+
const test = iscSplittingTest(rest);
|
|
443
|
+
if (!test.pass) {
|
|
444
|
+
emitObserveEvent('PaiSplittingTest', { criterion: rest, pass: false, reason: test.reason });
|
|
445
|
+
notify(`❌ ISC failed splitting test: ${test.reason}`, 'warning');
|
|
446
|
+
if (test.suggestion)
|
|
447
|
+
notify(`💡 ${test.suggestion}`, 'info');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
state.innerLoop.isc.push(rest);
|
|
451
|
+
persist(pi, 'pai-isc', { criterion: rest, phase: state.innerLoop.phase });
|
|
452
|
+
emitObserveEvent('PaiSplittingTest', { criterion: rest, pass: true });
|
|
453
|
+
notify(`📋 ISC-${state.innerLoop.isc.length}/${ISC_MINIMUMS[state.innerLoop.effort]}: ${rest}`, 'info');
|
|
454
|
+
updateWidget();
|
|
455
|
+
},
|
|
456
|
+
// v4.0.3 Feature #3: Anti-criteria
|
|
457
|
+
isca(rest) {
|
|
458
|
+
if (!state.innerLoop) {
|
|
459
|
+
notify('No active loop', 'error');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (!rest) {
|
|
463
|
+
notify('Usage: /pai isca <what must NOT happen>', 'error');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const severity = /critical|security|data.?loss|crash/i.test(rest) ? 'critical' : /error|fail|break/i.test(rest) ? 'high' : 'medium';
|
|
467
|
+
const ac = { id: `a${state.innerLoop.iscA.length}`, description: rest, severity };
|
|
468
|
+
state.innerLoop.iscA.push(ac);
|
|
469
|
+
persist(pi, 'pai-isc-anti', { anti: rest, severity });
|
|
470
|
+
emitObserveEvent('PaiIscGate', { type: 'anti', criterion: rest, severity });
|
|
471
|
+
notify(`🚫 ISC-A${state.innerLoop.iscA.length} [${severity}]: ${rest}`, 'info');
|
|
472
|
+
updateWidget();
|
|
473
|
+
},
|
|
474
|
+
// v4.0.3 Feature #4: Capability selection
|
|
475
|
+
capabilities(rest) {
|
|
476
|
+
if (!state.innerLoop) {
|
|
477
|
+
notify('No active loop', 'error');
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (!rest) {
|
|
481
|
+
// List current capabilities
|
|
482
|
+
if (!state.innerLoop.capabilities.size) {
|
|
483
|
+
notify('No capabilities selected. /pai capabilities add <tool|skill> <name> [min]', 'info');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const caps = Array.from(state.innerLoop.capabilities.values());
|
|
487
|
+
const list = caps.map(c => ` ${c.type}:${c.name} — ${c.invocations}/${c.minRequired ?? '∞'} invocations`).join('\n');
|
|
488
|
+
pi.sendMessage({ customType: 'pai-capabilities', content: `# Selected Capabilities\n\n${list}`, display: true, details: undefined }, { triggerTurn: false });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const parts = rest.split(/\s+/);
|
|
492
|
+
if (parts[0] === 'add' && parts.length >= 3) {
|
|
493
|
+
const type = parts[1];
|
|
494
|
+
const name = parts[2];
|
|
495
|
+
const min = parts[3] ? parseInt(parts[3], 10) : undefined;
|
|
496
|
+
if (type !== 'tool' && type !== 'skill') {
|
|
497
|
+
notify('Type must be "tool" or "skill"', 'error');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
state.innerLoop.capabilities.set(name, { name, type, minRequired: min, invocations: 0 });
|
|
501
|
+
emitObserveEvent('PaiCapability', { action: 'add', name, type, minRequired: min });
|
|
502
|
+
notify(`🔧 Capability: ${type}:${name}${min ? ` (min ${min})` : ''}`, 'info');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
notify('Usage: /pai capabilities add <tool|skill> <name> [min]', 'error');
|
|
506
|
+
},
|
|
507
|
+
next(rest) {
|
|
508
|
+
if (!state.innerLoop) {
|
|
509
|
+
notify('No active loop. /pai loop <goal>', 'error');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (rest)
|
|
513
|
+
state.innerLoop.data[state.innerLoop.phase] = rest;
|
|
514
|
+
const idx = PHASES.indexOf(state.innerLoop.phase);
|
|
515
|
+
// v4.0.3 Feature #2: ISC count gate before EXECUTE phase
|
|
516
|
+
if (state.innerLoop.phase === 'DECIDE') {
|
|
517
|
+
const min = ISC_MINIMUMS[state.innerLoop.effort];
|
|
518
|
+
const count = state.innerLoop.isc.length;
|
|
519
|
+
if (count < min) {
|
|
520
|
+
emitObserveEvent('PaiIscGate', { type: 'count', count, minimum: min, effort: state.innerLoop.effort, pass: false });
|
|
521
|
+
notify(`❌ ISC gate: ${count}/${min} criteria (${state.innerLoop.effort} requires ${min}). Add more with /pai isc`, 'error');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
emitObserveEvent('PaiIscGate', { type: 'count', count, minimum: min, effort: state.innerLoop.effort, pass: true });
|
|
525
|
+
}
|
|
526
|
+
// v4.0.3 Feature #5: Capability invocation check before VERIFY
|
|
527
|
+
if (state.innerLoop.phase === 'EXECUTE' && state.innerLoop.capabilities.size > 0) {
|
|
528
|
+
const unmet = [];
|
|
529
|
+
const capEntries = Array.from(state.innerLoop.capabilities.entries());
|
|
530
|
+
for (let ci = 0; ci < capEntries.length; ci++) {
|
|
531
|
+
const [name, cap] = capEntries[ci];
|
|
532
|
+
if (cap.minRequired && cap.invocations < cap.minRequired) {
|
|
533
|
+
unmet.push(`${cap.type}:${name} (${cap.invocations}/${cap.minRequired})`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (unmet.length) {
|
|
537
|
+
notify(`⚠️ Unmet capabilities: ${unmet.join(', ')}`, 'warning');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (idx < PHASES.length - 1) {
|
|
541
|
+
state.innerLoop.phase = PHASES[idx + 1];
|
|
542
|
+
emitObserveEvent('PaiPhaseTransition', { phase: state.innerLoop.phase, goal: state.innerLoop.goal, effort: state.innerLoop.effort });
|
|
543
|
+
notify(`→ ${state.innerLoop.phase}`, 'info');
|
|
544
|
+
// v4.0.3 Feature #6: Time budget check
|
|
545
|
+
const elapsedMin = (Date.now() - state.innerLoop.startTime) / 60000;
|
|
546
|
+
const budget = TIME_BUDGETS_MIN[state.innerLoop.effort];
|
|
547
|
+
if (elapsedMin > budget * TIME_COMPRESS_FACTOR) {
|
|
548
|
+
emitObserveEvent('PaiEffortCompress', { elapsed: +elapsedMin.toFixed(1), budget, effort: state.innerLoop.effort });
|
|
549
|
+
notify(`⏰ Over time budget (${Math.round(elapsedMin)}min / ${budget}min) — compressing scope`, 'warning');
|
|
550
|
+
}
|
|
551
|
+
else if (elapsedMin > budget) {
|
|
552
|
+
notify(`⏰ At time budget (${Math.round(elapsedMin)}min / ${budget}min)`, 'info');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
state.iterationCount++;
|
|
557
|
+
const elapsed = Math.round((Date.now() - state.innerLoop.startTime) / 1000);
|
|
558
|
+
// v4.0.3 Feature #5: Final capability report
|
|
559
|
+
const capReport = state.innerLoop.capabilities.size > 0
|
|
560
|
+
? Array.from(state.innerLoop.capabilities.values()).map(c => `${c.type}:${c.name}=${c.invocations}`).join(', ')
|
|
561
|
+
: 'none';
|
|
562
|
+
persist(pi, 'pai-loop-complete', {
|
|
563
|
+
goal: state.innerLoop.goal, iteration: state.iterationCount,
|
|
564
|
+
effort: state.innerLoop.effort, isc: state.innerLoop.isc,
|
|
565
|
+
iscA: state.innerLoop.iscA.map(a => a.description),
|
|
566
|
+
capabilities: capReport,
|
|
567
|
+
data: state.innerLoop.data, elapsed,
|
|
568
|
+
});
|
|
569
|
+
emitObserveEvent('PaiLoopComplete', {
|
|
570
|
+
goal: state.innerLoop.goal, iteration: state.iterationCount,
|
|
571
|
+
effort: state.innerLoop.effort, elapsed,
|
|
572
|
+
isc_count: state.innerLoop.isc.length,
|
|
573
|
+
isc_anti_count: state.innerLoop.iscA.length,
|
|
574
|
+
capabilities: capReport,
|
|
575
|
+
});
|
|
576
|
+
notify(`✅ Loop #${state.iterationCount} complete (${elapsed}s) | ${state.innerLoop.isc.length} ISC, ${state.innerLoop.iscA.length} anti`, 'info');
|
|
577
|
+
state.innerLoop = null;
|
|
578
|
+
}
|
|
579
|
+
updateWidget();
|
|
580
|
+
},
|
|
581
|
+
template(rest) {
|
|
582
|
+
const templates = loadTemplates();
|
|
583
|
+
const name = rest.trim().toLowerCase();
|
|
584
|
+
if (!name || !templates[name]) {
|
|
585
|
+
notify(`Templates: ${Object.keys(templates).join(', ')}`, 'info');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const t = templates[name];
|
|
589
|
+
state.mission = t.mission;
|
|
590
|
+
persist(pi, 'pai-mission', { mission: t.mission, template: name });
|
|
591
|
+
for (const title of t.goals) {
|
|
592
|
+
const id = `g${state.goals.size}`;
|
|
593
|
+
state.goals.set(id, { id, title, status: 'active', priority: 'p1', isc: [] });
|
|
594
|
+
persist(pi, 'pai-goal', { id, title, status: 'active' });
|
|
595
|
+
}
|
|
596
|
+
for (const title of t.challenges) {
|
|
597
|
+
const id = `c${state.challenges.size}`;
|
|
598
|
+
state.challenges.set(id, { id, title, severity: 'medium', affectedGoals: [] });
|
|
599
|
+
persist(pi, 'pai-challenge', { id, title });
|
|
600
|
+
}
|
|
601
|
+
notify(`📋 Template "${name}": ${t.goals.length} goals, ${t.challenges.length} challenges`, 'info');
|
|
602
|
+
updateWidget();
|
|
603
|
+
},
|
|
604
|
+
// Steal #2: Agent Personas
|
|
605
|
+
agent(rest) {
|
|
606
|
+
const name = rest.trim().toLowerCase();
|
|
607
|
+
if (!name || !AGENT_PERSONAS[name]) {
|
|
608
|
+
const list = Object.entries(AGENT_PERSONAS).map(([k, v]) => ` ${k}: ${v.role}`).join('\n');
|
|
609
|
+
pi.sendMessage({ customType: 'pai-agents', content: `# Available Agent Personas\n\n${list}\n\nUsage: /pai agent <name> <task>`, display: true, details: undefined }, { triggerTurn: false });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const taskStart = rest.indexOf(' ');
|
|
613
|
+
const task = taskStart > 0 ? rest.slice(taskStart + 1).trim() : '';
|
|
614
|
+
if (!task) {
|
|
615
|
+
notify(`Usage: /pai agent ${name} <task description>`, 'error');
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const persona = AGENT_PERSONAS[name];
|
|
619
|
+
pi.sendMessage({
|
|
620
|
+
customType: 'pai-agent-dispatch',
|
|
621
|
+
content: `# ${persona.name} Mode\n\n**System:** ${persona.systemPrompt}\n\n**Task:** ${task}`,
|
|
622
|
+
display: true,
|
|
623
|
+
details: undefined,
|
|
624
|
+
}, { triggerTurn: true });
|
|
625
|
+
notify(`🎭 ${persona.name}: ${task.slice(0, 60)}...`, 'info');
|
|
626
|
+
},
|
|
627
|
+
// Steal #5: Plans directory
|
|
628
|
+
plans(rest, ctx) {
|
|
629
|
+
const plans = listPlans(ctx.cwd);
|
|
630
|
+
if (!plans.length) {
|
|
631
|
+
notify('No plans in .pi/plans/ — use brainstorm skill to create one', 'info');
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const list = plans.map(p => `- ${p}`).join('\n');
|
|
635
|
+
pi.sendMessage({ customType: 'pai-plans', content: `# Plans\n\n${list}\n\nPlans dir: .pi/plans/`, display: true, details: undefined }, { triggerTurn: false });
|
|
636
|
+
},
|
|
637
|
+
// Steal #4: Self-evolution
|
|
638
|
+
evolve() {
|
|
639
|
+
const patterns = detectRepeatingPatterns(state.learnings);
|
|
640
|
+
if (!patterns.length) {
|
|
641
|
+
notify('No repeating patterns detected yet', 'info');
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const report = patterns.map((p, i) => `${i + 1}. "${p}" (3+ occurrences)`).join('\n');
|
|
645
|
+
const sentimentDist = { positive: 0, neutral: 0, negative: 0 };
|
|
646
|
+
for (const l of state.learnings)
|
|
647
|
+
sentimentDist[l.sentiment || 'neutral']++;
|
|
648
|
+
pi.sendMessage({
|
|
649
|
+
customType: 'pai-evolve',
|
|
650
|
+
content: `# Evolution Trigger Report\n\n## Repeating Learning Patterns\n${report}\n\n## Sentiment Distribution\n- Positive: ${sentimentDist.positive}\n- Neutral: ${sentimentDist.neutral}\n- Negative: ${sentimentDist.negative}\n\n## Recommended Actions\nThese patterns indicate recurring issues. Consider:\n1. **Run /gepa** on related skills to evolve them\n2. **Create a new skill** to address the pattern\n3. **Update AGENTS.md** with the learning\n\nUse \`pi-gepa\` extension for automated skill evolution.`,
|
|
651
|
+
display: true,
|
|
652
|
+
details: undefined,
|
|
653
|
+
}, { triggerTurn: false });
|
|
654
|
+
},
|
|
655
|
+
// Steal #3: Detailed trend report
|
|
656
|
+
trend() {
|
|
657
|
+
if (!state.ratings.length) {
|
|
658
|
+
notify('No ratings yet — use /rate <1-10> after tasks', 'info');
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const { trend, avg, recent } = ratingTrend(state.ratings);
|
|
662
|
+
const sentimentDist = { positive: 0, neutral: 0, negative: 0 };
|
|
663
|
+
for (const r of state.ratings)
|
|
664
|
+
sentimentDist[r.sentiment]++;
|
|
665
|
+
const recentRatings = state.ratings.slice(-10).map(r => {
|
|
666
|
+
const icon = r.sentiment === 'positive' ? '😊' : r.sentiment === 'negative' ? '😞' : '😐';
|
|
667
|
+
return `- ⭐${r.score} ${icon} ${r.context || '(no context)'}`;
|
|
668
|
+
}).join('\n');
|
|
669
|
+
pi.sendMessage({
|
|
670
|
+
customType: 'pai-trend',
|
|
671
|
+
content: `# Rating Trend\n\n**Overall:** ⭐${avg} (${state.ratings.length} ratings)\n**Recent (last 5):** ⭐${recent} ${trend === 'improving' ? '📈 Improving' : trend === 'declining' ? '📉 Declining' : '➡️ Stable'}\n\n## Sentiment\n- 😊 Positive: ${sentimentDist.positive}\n- 😐 Neutral: ${sentimentDist.neutral}\n- 😞 Negative: ${sentimentDist.negative}\n\n## Recent Ratings\n${recentRatings}`,
|
|
672
|
+
display: true,
|
|
673
|
+
details: undefined,
|
|
674
|
+
}, { triggerTurn: false });
|
|
675
|
+
},
|
|
676
|
+
reset() {
|
|
677
|
+
state.mission = null;
|
|
678
|
+
state.goals.clear();
|
|
679
|
+
state.challenges.clear();
|
|
680
|
+
state.learnings = [];
|
|
681
|
+
state.ratings = [];
|
|
682
|
+
state.innerLoop = null;
|
|
683
|
+
state.iterationCount = 0;
|
|
684
|
+
state.ralphIteration = 0;
|
|
685
|
+
state.ralphActive = false;
|
|
686
|
+
persist(pi, 'pai-reset', {});
|
|
687
|
+
notify('🗑️ PAI state reset', 'warning');
|
|
688
|
+
updateWidget();
|
|
689
|
+
},
|
|
690
|
+
status(_rest, ctx) {
|
|
691
|
+
const goals = Array.from(state.goals.values());
|
|
692
|
+
const challenges = Array.from(state.challenges.values());
|
|
693
|
+
const { trend, avg, recent } = ratingTrend(state.ratings);
|
|
694
|
+
const trendIcon = trend === 'improving' ? '📈' : trend === 'declining' ? '📉' : '➡️';
|
|
695
|
+
const patterns = detectRepeatingPatterns(state.learnings);
|
|
696
|
+
const plans = listPlans(ctx.cwd);
|
|
697
|
+
let r = `# PAI Status (v4.2 — full v4.0.3 sync: splitting test, count gate, anti-criteria, capabilities, time budgets)\n\n`;
|
|
698
|
+
r += `**Mission:** ${state.mission || 'Not set'}\n`;
|
|
699
|
+
r += `**Iterations:** ${state.iterationCount} | **Rating:** ⭐${avg} ${trendIcon}${recent} (${state.ratings.length} signals)\n`;
|
|
700
|
+
if (patterns.length)
|
|
701
|
+
r += `**⚠️ Repeating patterns:** ${patterns.length} — run /pai evolve\n`;
|
|
702
|
+
r += '\n';
|
|
703
|
+
r += `## Goals (${goals.length})\n`;
|
|
704
|
+
for (const g of goals) {
|
|
705
|
+
const icon = g.status === 'completed' ? '✅' : g.status === 'blocked' ? '🚫' : '🎯';
|
|
706
|
+
r += `- ${icon} **${g.id}** ${g.title} (${g.status})\n`;
|
|
707
|
+
}
|
|
708
|
+
r += `\n## Challenges (${challenges.length})\n`;
|
|
709
|
+
for (const c of challenges)
|
|
710
|
+
r += `- ⚠️ **${c.id}** ${c.title}\n`;
|
|
711
|
+
r += `\n## Recent Learnings\n`;
|
|
712
|
+
for (const l of state.learnings.slice(-5)) {
|
|
713
|
+
const sIcon = l.sentiment === 'positive' ? '😊' : l.sentiment === 'negative' ? '😞' : '📚';
|
|
714
|
+
r += `- ${sIcon} [${l.category}] ${l.insight}${l.fromRating ? ` (⭐${l.fromRating})` : ''}\n`;
|
|
715
|
+
}
|
|
716
|
+
if (state.innerLoop) {
|
|
717
|
+
const min = ISC_MINIMUMS[state.innerLoop.effort];
|
|
718
|
+
const budget = TIME_BUDGETS_MIN[state.innerLoop.effort];
|
|
719
|
+
const elapsedMin = Math.round((Date.now() - state.innerLoop.startTime) / 60000);
|
|
720
|
+
r += `\n## Active Loop (v4.0.3 Algorithm)\n`;
|
|
721
|
+
r += `**Phase:** ${state.innerLoop.phase} | **Effort:** ${state.innerLoop.effort} | **Goal:** ${state.innerLoop.goal}\n`;
|
|
722
|
+
r += `**ISC:** ${state.innerLoop.isc.length}/${min} | **Anti:** ${state.innerLoop.iscA.length} | **Time:** ${elapsedMin}/${budget}min\n`;
|
|
723
|
+
r += `**Phases:** OBSERVE → PLAN → DECIDE → EXECUTE → VERIFY\n`;
|
|
724
|
+
for (let ii = 0; ii < state.innerLoop.isc.length; ii++)
|
|
725
|
+
r += `- ISC-${ii + 1}: ${state.innerLoop.isc[ii]}\n`;
|
|
726
|
+
if (state.innerLoop.iscA.length) {
|
|
727
|
+
r += `\n**Anti-Criteria (must NOT happen):**\n`;
|
|
728
|
+
for (const ac of state.innerLoop.iscA)
|
|
729
|
+
r += `- 🚫 [${ac.severity}] ${ac.description}\n`;
|
|
730
|
+
}
|
|
731
|
+
if (state.innerLoop.capabilities.size) {
|
|
732
|
+
r += `\n**Capabilities:**\n`;
|
|
733
|
+
const statusCaps = Array.from(state.innerLoop.capabilities.values());
|
|
734
|
+
for (let ci = 0; ci < statusCaps.length; ci++) {
|
|
735
|
+
const cap = statusCaps[ci];
|
|
736
|
+
r += `- 🔧 ${cap.type}:${cap.name} — ${cap.invocations}/${cap.minRequired ?? '∞'}\n`;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
r += `\n## Agent Personas\n${Object.entries(AGENT_PERSONAS).map(([k, v]) => `- **${k}**: ${v.role}`).join('\n')}\n`;
|
|
741
|
+
if (plans.length)
|
|
742
|
+
r += `\n## Plans (.pi/plans/)\n${plans.slice(0, 5).map(p => `- ${p}`).join('\n')}\n`;
|
|
743
|
+
r += `\n## Damage Control\n${rules.bashToolPatterns.length} bash | ${rules.zeroAccessPaths.length} zero-access | ${rules.readOnlyPaths.length} read-only | ${rules.noDeletePaths.length} no-delete\n`;
|
|
744
|
+
pi.sendMessage({ customType: 'pai-status', content: r, display: true, details: undefined }, { triggerTurn: false });
|
|
745
|
+
},
|
|
746
|
+
};
|
|
747
|
+
// ── /pai command ───────────────────────────────────────────────────────
|
|
748
|
+
pi.registerCommand('pai', {
|
|
749
|
+
description: 'PAI v4.2: /pai mission|goal|done|block|challenge|learn|loop|next|isc|isca|effort|capabilities|template|agent|plans|trend|evolve|sessions|replay|reset|status',
|
|
750
|
+
handler: async (args, ctx) => {
|
|
751
|
+
widgetCtx = ctx;
|
|
752
|
+
ensurePlansDir(ctx.cwd);
|
|
753
|
+
const parts = (args || '').trim().split(/\s+/);
|
|
754
|
+
const sub = parts[0]?.toLowerCase();
|
|
755
|
+
const rest = parts.slice(1).join(' ');
|
|
756
|
+
const fn = paiCommands[sub];
|
|
757
|
+
if (fn)
|
|
758
|
+
fn(rest, ctx);
|
|
759
|
+
else
|
|
760
|
+
notify(`/pai ${Object.keys(paiCommands).join('|')}`, 'info');
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
// ── /rate (enhanced with sentiment) ────────────────────────────────────
|
|
764
|
+
pi.registerCommand('rate', {
|
|
765
|
+
description: 'Rate last output 1-10 with sentiment: /rate <score> [context]',
|
|
766
|
+
handler: async (args, ctx) => {
|
|
767
|
+
widgetCtx = ctx;
|
|
768
|
+
const parts = (args || '').trim().split(/\s+/);
|
|
769
|
+
const score = parseInt(parts[0], 10);
|
|
770
|
+
const context = parts.slice(1).join(' ');
|
|
771
|
+
if (isNaN(score) || score < 1 || score > 10) {
|
|
772
|
+
notify('Usage: /rate <1-10> [context]', 'error');
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const sentiment = inferSentiment(score, context);
|
|
776
|
+
state.ratings.push({ score, context, timestamp: new Date(), sentiment });
|
|
777
|
+
persist(pi, 'pai-rating', { score, context, sentiment });
|
|
778
|
+
emitObserveEvent('PaiRating', { score, context, sentiment });
|
|
779
|
+
if (score <= 3) {
|
|
780
|
+
const l = { insight: `Low rating (${score}): ${context || 'below expectations'}`, confidence: 0.9, category: 'algorithm', timestamp: new Date(), fromRating: score, sentiment: 'negative' };
|
|
781
|
+
state.learnings.push(l);
|
|
782
|
+
persist(pi, 'pai-learning', { insight: l.insight, category: 'algorithm', fromRating: score, sentiment: 'negative' });
|
|
783
|
+
notify(`⭐${score} 😞 — Learning captured`, 'warning');
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
const sIcon = sentiment === 'positive' ? '😊' : '😐';
|
|
787
|
+
notify(`⭐${score} ${sIcon}${score >= 8 ? ' — Excellent!' : ''}`, 'info');
|
|
788
|
+
}
|
|
789
|
+
updateWidget();
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
// ── /ralph ─────────────────────────────────────────────────────────────
|
|
793
|
+
pi.registerCommand('ralph', {
|
|
794
|
+
description: 'Ralph Wiggum iteration: /ralph <task> or /ralph stop',
|
|
795
|
+
handler: async (args, ctx) => {
|
|
796
|
+
widgetCtx = ctx;
|
|
797
|
+
const task = (args || '').trim();
|
|
798
|
+
if (task.toLowerCase() === 'stop') {
|
|
799
|
+
state.ralphActive = false;
|
|
800
|
+
notify(`🛑 Ralph stopped after ${state.ralphIteration} iterations`, 'warning');
|
|
801
|
+
updateWidget();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (!task) {
|
|
805
|
+
notify('Usage: /ralph <task> or /ralph stop', 'error');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
state.ralphActive = true;
|
|
809
|
+
state.ralphIteration = 0;
|
|
810
|
+
notify(`🔄 Ralph starting: ${task}`, 'info');
|
|
811
|
+
updateWidget();
|
|
812
|
+
pi.sendMessage({ customType: 'pai-ralph', content: `[Ralph #${++state.ralphIteration}]\n\nTask: ${task}\n\nExecute this task. Say "RALPH_DONE" when finished.`, display: true, details: undefined }, { triggerTurn: true });
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
pi.on('message_end', async (event) => {
|
|
816
|
+
if (!state.ralphActive)
|
|
817
|
+
return;
|
|
818
|
+
if (state.ralphIteration >= 50) {
|
|
819
|
+
state.ralphActive = false;
|
|
820
|
+
notify('🛑 Ralph: 50 limit', 'warning');
|
|
821
|
+
updateWidget();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const text = typeof event === 'object' && event !== null && 'text' in event ? String(event.text) : '';
|
|
825
|
+
if (text.includes('RALPH_DONE')) {
|
|
826
|
+
state.ralphActive = false;
|
|
827
|
+
notify(`✅ Ralph done in ${state.ralphIteration}`, 'info');
|
|
828
|
+
updateWidget();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
pi.sendMessage({ customType: 'pai-ralph', content: `[Ralph #${++state.ralphIteration}] Continue. Say "RALPH_DONE" when finished.`, display: true, details: undefined }, { triggerTurn: true });
|
|
832
|
+
updateWidget();
|
|
833
|
+
});
|
|
834
|
+
// ── Tools ──────────────────────────────────────────────────────────────
|
|
835
|
+
pi.registerTool({
|
|
836
|
+
name: 'pai_status',
|
|
837
|
+
label: 'PAI Status',
|
|
838
|
+
description: 'Get PAI status: mission, goals, challenges, learnings, loop, ratings, trends, personas.',
|
|
839
|
+
parameters: Type.Object({}),
|
|
840
|
+
execute: async () => {
|
|
841
|
+
const { trend, avg, recent } = ratingTrend(state.ratings);
|
|
842
|
+
const patterns = detectRepeatingPatterns(state.learnings);
|
|
843
|
+
return {
|
|
844
|
+
details: undefined,
|
|
845
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
846
|
+
version: '4.2.0',
|
|
847
|
+
algorithm: 'OBSERVE → PLAN → DECIDE → EXECUTE → VERIFY',
|
|
848
|
+
effortLevels: 'Standard(8)|Extended(16)|Advanced(24)|Deep(40)|Comprehensive(64)',
|
|
849
|
+
iscMethodology: 'splitting test + count gate + anti-criteria + capability tracking',
|
|
850
|
+
timeBudgets: TIME_BUDGETS_MIN,
|
|
851
|
+
mission: state.mission,
|
|
852
|
+
goals: Array.from(state.goals.values()),
|
|
853
|
+
challenges: Array.from(state.challenges.values()),
|
|
854
|
+
learnings: state.learnings.slice(-10).map(l => ({ insight: l.insight, category: l.category, sentiment: l.sentiment })),
|
|
855
|
+
innerLoop: state.innerLoop ? {
|
|
856
|
+
phase: state.innerLoop.phase, effort: state.innerLoop.effort, goal: state.innerLoop.goal,
|
|
857
|
+
isc: state.innerLoop.isc,
|
|
858
|
+
iscMinimum: ISC_MINIMUMS[state.innerLoop.effort],
|
|
859
|
+
iscAnti: state.innerLoop.iscA.map(a => ({ severity: a.severity, description: a.description })),
|
|
860
|
+
capabilities: Array.from(state.innerLoop.capabilities.values()).map(c => ({ name: c.name, type: c.type, invocations: c.invocations, minRequired: c.minRequired })),
|
|
861
|
+
elapsedMin: +((Date.now() - state.innerLoop.startTime) / 60000).toFixed(1),
|
|
862
|
+
timeBudgetMin: TIME_BUDGETS_MIN[state.innerLoop.effort],
|
|
863
|
+
} : null,
|
|
864
|
+
iterations: state.iterationCount,
|
|
865
|
+
ratings: { avg, recent, trend: trend, count: state.ratings.length },
|
|
866
|
+
repeatingPatterns: patterns,
|
|
867
|
+
agentPersonas: Object.keys(AGENT_PERSONAS),
|
|
868
|
+
}, null, 2) }],
|
|
869
|
+
};
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
pi.registerTool({
|
|
873
|
+
name: 'pai_learn',
|
|
874
|
+
label: 'PAI Learn',
|
|
875
|
+
description: 'Record a learning/insight into PAI.',
|
|
876
|
+
parameters: Type.Object({
|
|
877
|
+
insight: Type.String({ description: 'The learning or insight' }),
|
|
878
|
+
category: Type.Optional(Type.String({ description: 'algorithm|system|domain|process' })),
|
|
879
|
+
confidence: Type.Optional(Type.Number({ description: '0-1' })),
|
|
880
|
+
}),
|
|
881
|
+
execute: async (_callId, args) => {
|
|
882
|
+
const sentiment = inferSentiment(5, args.insight);
|
|
883
|
+
state.learnings.push({ insight: args.insight, confidence: args.confidence ?? 0.8, category: args.category || 'domain', timestamp: new Date(), sentiment });
|
|
884
|
+
persist(pi, 'pai-learning', { insight: args.insight, category: args.category || 'domain', sentiment });
|
|
885
|
+
updateWidget();
|
|
886
|
+
return { details: undefined, content: [{ type: 'text', text: `Learning [${args.category || 'domain'}] ${sentiment}: ${args.insight}` }] };
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
pi.registerTool({
|
|
890
|
+
name: 'pai_rate',
|
|
891
|
+
label: 'PAI Rate',
|
|
892
|
+
description: 'Rate output quality 1-10 with sentiment tracking. Low ratings auto-capture learnings.',
|
|
893
|
+
parameters: Type.Object({
|
|
894
|
+
score: Type.Number({ description: 'Rating 1-10' }),
|
|
895
|
+
context: Type.Optional(Type.String({ description: 'Why' })),
|
|
896
|
+
}),
|
|
897
|
+
execute: async (_callId, args) => {
|
|
898
|
+
const score = Math.max(1, Math.min(10, Math.round(args.score)));
|
|
899
|
+
const sentiment = inferSentiment(score, args.context || '');
|
|
900
|
+
state.ratings.push({ score, context: args.context || '', timestamp: new Date(), sentiment });
|
|
901
|
+
persist(pi, 'pai-rating', { score, context: args.context, sentiment });
|
|
902
|
+
if (score <= 3) {
|
|
903
|
+
state.learnings.push({ insight: `Low rating (${score}): ${args.context || 'below expectations'}`, confidence: 0.9, category: 'algorithm', timestamp: new Date(), fromRating: score, sentiment: 'negative' });
|
|
904
|
+
persist(pi, 'pai-learning', { insight: `Low rating (${score})`, category: 'algorithm', fromRating: score, sentiment: 'negative' });
|
|
905
|
+
}
|
|
906
|
+
updateWidget();
|
|
907
|
+
const { trend, avg } = ratingTrend(state.ratings);
|
|
908
|
+
return { details: undefined, content: [{ type: 'text', text: `Rated ⭐${score} ${sentiment} | Avg: ${avg} (${trend})${args.context ? ' — ' + args.context : ''}` }] };
|
|
909
|
+
},
|
|
910
|
+
});
|
|
911
|
+
// ── Damage Control ─────────────────────────────────────────────────────
|
|
912
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
913
|
+
const { isToolCallEventType } = await import('@mariozechner/pi-coding-agent');
|
|
914
|
+
// Emit tool call to observability dashboard
|
|
915
|
+
const toolName = event?.name || event?.tool || 'unknown';
|
|
916
|
+
emitObserveEvent('PostToolUse', { tool_name: toolName, source: 'pi-pai' });
|
|
917
|
+
// v4.0.3 Feature #5: Track capability invocations
|
|
918
|
+
if (state.innerLoop?.capabilities.size) {
|
|
919
|
+
const cap = state.innerLoop.capabilities.get(toolName);
|
|
920
|
+
if (cap) {
|
|
921
|
+
cap.invocations++;
|
|
922
|
+
emitObserveEvent('PaiCapability', { action: 'invoke', name: toolName, count: cap.invocations });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
if (isToolCallEventType('bash', event)) {
|
|
926
|
+
const cmd = event.input.command || '';
|
|
927
|
+
for (const rule of rules.bashToolPatterns) {
|
|
928
|
+
try {
|
|
929
|
+
if (new RegExp(rule.pattern).test(cmd)) {
|
|
930
|
+
if (rule.ask) {
|
|
931
|
+
const ok = await ctx.ui.confirm('🛡️ PAI', `${rule.reason}\n\n${cmd}\n\nAllow?`, { timeout: 30000 });
|
|
932
|
+
if (!ok) {
|
|
933
|
+
persist(pi, 'pai-dc', { cmd, reason: rule.reason, action: 'denied' });
|
|
934
|
+
ctx.abort();
|
|
935
|
+
return { block: true, reason: `🛑 ${rule.reason}. DO NOT retry.` };
|
|
936
|
+
}
|
|
937
|
+
return { block: false };
|
|
938
|
+
}
|
|
939
|
+
persist(pi, 'pai-dc', { cmd, reason: rule.reason, action: 'blocked' });
|
|
940
|
+
ctx.abort();
|
|
941
|
+
return { block: true, reason: `🛑 ${rule.reason}. DO NOT retry.` };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
catch { /* bad regex, skip */ }
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (isToolCallEventType('read', event) || isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
|
|
948
|
+
const filePath = event.input.path || '';
|
|
949
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(ctx.cwd, filePath);
|
|
950
|
+
for (const zap of rules.zeroAccessPaths) {
|
|
951
|
+
if (isPathMatch(resolved, zap, ctx.cwd)) {
|
|
952
|
+
ctx.abort();
|
|
953
|
+
return { block: true, reason: `🛑 zero-access: ${zap}. DO NOT retry.` };
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
|
|
957
|
+
for (const rop of rules.readOnlyPaths) {
|
|
958
|
+
if (isPathMatch(resolved, rop, ctx.cwd)) {
|
|
959
|
+
ctx.abort();
|
|
960
|
+
return { block: true, reason: `🛑 read-only: ${rop}. DO NOT modify.` };
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (isToolCallEventType('bash', event)) {
|
|
966
|
+
const cmd = event.input.command || '';
|
|
967
|
+
if (/\b(rm|del|rmdir|Remove-Item)\b/i.test(cmd)) {
|
|
968
|
+
for (const ndp of rules.noDeletePaths) {
|
|
969
|
+
const clean = ndp.replace(/^~\//, '').replace(/^\*/, '');
|
|
970
|
+
if (clean && cmd.includes(clean)) {
|
|
971
|
+
persist(pi, 'pai-dc', { cmd, reason: `no-delete: ${ndp}`, action: 'blocked' });
|
|
972
|
+
ctx.abort();
|
|
973
|
+
return { block: true, reason: `🛑 no-delete: ${ndp}. DO NOT retry.` };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return { block: false };
|
|
979
|
+
});
|
|
980
|
+
// ── Session lifecycle ──────────────────────────────────────────────────
|
|
981
|
+
// ── /pai sessions — list and replay pi sessions into the dashboard ───
|
|
982
|
+
paiCommands['sessions'] = function (_rest, ctx) {
|
|
983
|
+
const sessDir = path.join(os.homedir(), '.pi', 'agent', 'sessions');
|
|
984
|
+
if (!fs.existsSync(sessDir)) {
|
|
985
|
+
notify('No sessions found', 'info');
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
// Find session dirs and get latest file from each
|
|
989
|
+
const dirs = fs.readdirSync(sessDir).filter(d => {
|
|
990
|
+
try {
|
|
991
|
+
return fs.statSync(path.join(sessDir, d)).isDirectory();
|
|
992
|
+
}
|
|
993
|
+
catch {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
const sessions = [];
|
|
998
|
+
for (const dir of dirs) {
|
|
999
|
+
const files = fs.readdirSync(path.join(sessDir, dir)).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
1000
|
+
if (files.length) {
|
|
1001
|
+
const filePath = path.join(sessDir, dir, files[0]);
|
|
1002
|
+
try {
|
|
1003
|
+
const stat = fs.statSync(filePath);
|
|
1004
|
+
const lines = fs.readFileSync(filePath, 'utf8').trim().split('\n').length;
|
|
1005
|
+
sessions.push({ dir, file: files[0], time: stat.mtime, lines });
|
|
1006
|
+
}
|
|
1007
|
+
catch { /* skip */ }
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
sessions.sort((a, b) => b.time.getTime() - a.time.getTime());
|
|
1011
|
+
let r = `# Pi Sessions (${sessions.length})\n\n`;
|
|
1012
|
+
r += `| # | Project | File | Events | Last Modified |\n|---|---------|------|--------|---------------|\n`;
|
|
1013
|
+
const displaySessions = sessions.slice(0, 15);
|
|
1014
|
+
for (let si = 0; si < displaySessions.length; si++) {
|
|
1015
|
+
const s = displaySessions[si];
|
|
1016
|
+
const proj = s.dir.replace(/^--/, '').replace(/--$/, '').replace(/-/g, '/').slice(0, 40);
|
|
1017
|
+
r += `| ${si + 1} | ${proj} | ${s.file.slice(0, 30)} | ${s.lines} | ${s.time.toLocaleString()} |\n`;
|
|
1018
|
+
}
|
|
1019
|
+
r += `\n**Replay to dashboard:** \`/pai replay <session-number>\`\n`;
|
|
1020
|
+
r += `**Session dir:** ~/.pi/agent/sessions/\n`;
|
|
1021
|
+
pi.sendMessage({ customType: 'pai-sessions', content: r, display: true, details: undefined }, { triggerTurn: false });
|
|
1022
|
+
};
|
|
1023
|
+
paiCommands['replay'] = function (rest) {
|
|
1024
|
+
const sessDir = path.join(os.homedir(), '.pi', 'agent', 'sessions');
|
|
1025
|
+
if (!fs.existsSync(sessDir)) {
|
|
1026
|
+
notify('No sessions found', 'error');
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
// Find all sessions sorted by time
|
|
1030
|
+
const dirs = fs.readdirSync(sessDir).filter(d => {
|
|
1031
|
+
try {
|
|
1032
|
+
return fs.statSync(path.join(sessDir, d)).isDirectory();
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
return false;
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
const sessions = [];
|
|
1039
|
+
for (const dir of dirs) {
|
|
1040
|
+
const files = fs.readdirSync(path.join(sessDir, dir)).filter(f => f.endsWith('.jsonl')).sort().reverse();
|
|
1041
|
+
if (files.length)
|
|
1042
|
+
sessions.push(path.join(sessDir, dir, files[0]));
|
|
1043
|
+
}
|
|
1044
|
+
sessions.sort((a, b) => {
|
|
1045
|
+
try {
|
|
1046
|
+
return fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime();
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return 0;
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
const idx = parseInt(rest.trim()) - 1;
|
|
1053
|
+
if (isNaN(idx) || idx < 0 || idx >= sessions.length) {
|
|
1054
|
+
notify(`Usage: /pai replay <1-${sessions.length}>`, 'error');
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
const sessionFile = sessions[idx];
|
|
1058
|
+
let replayed = 0;
|
|
1059
|
+
try {
|
|
1060
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').trim().split('\n');
|
|
1061
|
+
for (const line of lines) {
|
|
1062
|
+
try {
|
|
1063
|
+
const ev = JSON.parse(line);
|
|
1064
|
+
// Map pi session events to observability format
|
|
1065
|
+
if (ev.type === 'session') {
|
|
1066
|
+
emitObserveEvent('SessionStart', { cwd: ev.cwd, source: 'pi-session-replay', session_id: ev.id });
|
|
1067
|
+
replayed++;
|
|
1068
|
+
}
|
|
1069
|
+
else if (ev.type === 'message' && ev.message?.content) {
|
|
1070
|
+
for (const c of ev.message.content) {
|
|
1071
|
+
if (c.type === 'toolCall') {
|
|
1072
|
+
emitObserveEvent('PostToolUse', {
|
|
1073
|
+
tool_name: c.name,
|
|
1074
|
+
tool_input: c.arguments,
|
|
1075
|
+
source: 'pi-session-replay',
|
|
1076
|
+
session_id: ev.id,
|
|
1077
|
+
});
|
|
1078
|
+
replayed++;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// Emit token usage
|
|
1082
|
+
if (ev.message?.usage) {
|
|
1083
|
+
emitObserveEvent('PaiTokenUsage', {
|
|
1084
|
+
input: ev.message.usage.input,
|
|
1085
|
+
output: ev.message.usage.output,
|
|
1086
|
+
cost: ev.message.usage.cost,
|
|
1087
|
+
model: ev.message.model,
|
|
1088
|
+
source: 'pi-session-replay',
|
|
1089
|
+
});
|
|
1090
|
+
replayed++;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
else if (ev.type === 'compaction') {
|
|
1094
|
+
emitObserveEvent('PaiCompaction', { source: 'pi-session-replay' });
|
|
1095
|
+
replayed++;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch { /* skip bad lines */ }
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
catch (e) {
|
|
1102
|
+
notify(`Failed to read session: ${e.message}`, 'error');
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
notify(`📊 Replayed ${replayed} events from ${path.basename(sessionFile)} → dashboard`, 'info');
|
|
1106
|
+
};
|
|
1107
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
1108
|
+
widgetCtx = ctx;
|
|
1109
|
+
rules = loadDamageRules(ctx.cwd);
|
|
1110
|
+
ensurePlansDir(ctx.cwd);
|
|
1111
|
+
observeSessionId = `pi-pai-${Date.now().toString(36)}`;
|
|
1112
|
+
emitObserveEvent('SessionStart', { cwd: ctx.cwd, source: 'pi-pai', version: '4.1.0' });
|
|
1113
|
+
updateWidget();
|
|
1114
|
+
const n = rules.bashToolPatterns.length + rules.zeroAccessPaths.length + rules.readOnlyPaths.length + rules.noDeletePaths.length;
|
|
1115
|
+
ctx.ui.notify(`🧠 π-PAI v4.2 (v4.0.3 full sync) | ${n ? n + ' rules' : 'no rules'} | /pai /ralph /rate`, 'info');
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
//# sourceMappingURL=extension.js.map
|