pi-soly 1.8.0 → 1.9.1
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/commands.ts +45 -1
- package/index.ts +11 -10
- package/intent.ts +106 -0
- package/package.json +1 -1
package/commands.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// =============================================================================
|
|
4
4
|
//
|
|
5
5
|
// Registers slash commands (via pi.registerCommand):
|
|
6
|
-
// - /rules manage soly rules (list/show/analytics/reload/enable/disable/add/new)
|
|
6
|
+
// - /rules manage soly rules (list/show/stats/analytics/reload/enable/disable/add/new)
|
|
7
|
+
// - /docs manage soly intent docs (stats — context breakdown)
|
|
7
8
|
// - /soly project state inspection (position/plan/phases/tasks/...)
|
|
8
9
|
// subcommands: position, state, plan, context, research, roadmap,
|
|
9
10
|
// progress, phases, tasks, task <id>, features,
|
|
@@ -32,6 +33,13 @@ import {
|
|
|
32
33
|
type RuleFile,
|
|
33
34
|
type SolyState,
|
|
34
35
|
} from "./core.ts";
|
|
36
|
+
import {
|
|
37
|
+
buildIntentStats,
|
|
38
|
+
formatIntentStats,
|
|
39
|
+
loadInlineIntentBodies,
|
|
40
|
+
type IntentDoc,
|
|
41
|
+
type IntentInlineDoc,
|
|
42
|
+
} from "./intent.ts";
|
|
35
43
|
import type { SolyConfig } from "./config.ts";
|
|
36
44
|
import { migrateSolyDir } from "./migrate.js";
|
|
37
45
|
import { initSolyProject } from "./init.js";
|
|
@@ -53,6 +61,7 @@ export interface CommandsDeps {
|
|
|
53
61
|
refreshState: () => void;
|
|
54
62
|
updateStatus: (ui: CommandUI) => void;
|
|
55
63
|
getConfig: () => SolyConfig;
|
|
64
|
+
getIntentDocs: () => IntentDoc[];
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
@@ -64,6 +73,7 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
64
73
|
refreshState,
|
|
65
74
|
updateStatus,
|
|
66
75
|
getConfig,
|
|
76
|
+
getIntentDocs,
|
|
67
77
|
} = deps;
|
|
68
78
|
|
|
69
79
|
// ============================================================================
|
|
@@ -347,6 +357,40 @@ What must the LLM do?
|
|
|
347
357
|
},
|
|
348
358
|
});
|
|
349
359
|
|
|
360
|
+
// ============================================================================
|
|
361
|
+
// /docs — manage soly intent docs (stats subcommand shows context usage)
|
|
362
|
+
// ============================================================================
|
|
363
|
+
pi.registerCommand("docs", {
|
|
364
|
+
description:
|
|
365
|
+
"manage soly intent docs (stats — show context breakdown)",
|
|
366
|
+
handler: async (args, ctx) => {
|
|
367
|
+
const ui: CommandUI = {
|
|
368
|
+
notify: (t, k) => ctx.ui.notify(t, k ?? "info"),
|
|
369
|
+
select: async (label, options) => {
|
|
370
|
+
const result = await ctx.ui.select(label, options);
|
|
371
|
+
return result === undefined ? null : options.indexOf(result);
|
|
372
|
+
},
|
|
373
|
+
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
374
|
+
};
|
|
375
|
+
const parts = args.trim().split(/\s+/);
|
|
376
|
+
const sub = parts[0] ?? "stats";
|
|
377
|
+
|
|
378
|
+
if (sub === "stats") {
|
|
379
|
+
const docs = getIntentDocs();
|
|
380
|
+
const inlineBodies: IntentInlineDoc[] = loadInlineIntentBodies(docs);
|
|
381
|
+
const stats = buildIntentStats(docs, inlineBodies);
|
|
382
|
+
ui.notify(formatIntentStats(stats), "info");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
ui.notify(
|
|
387
|
+
`Usage: /docs stats — show context breakdown for intent docs\n` +
|
|
388
|
+
`Found ${getIntentDocs().length} doc(s) loaded.`,
|
|
389
|
+
"info",
|
|
390
|
+
);
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
350
394
|
// ============================================================================
|
|
351
395
|
// /soly migrate — move .soly/ → .agents/ atomically
|
|
352
396
|
// ============================================================================
|
package/index.ts
CHANGED
|
@@ -128,6 +128,8 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
128
128
|
// Behavioral nudge state
|
|
129
129
|
let nudgeActiveForTask = false;
|
|
130
130
|
let editedFilesThisTurn = new Set<string>();
|
|
131
|
+
let lastEditedFiles: string[] = [];
|
|
132
|
+
let lastTurnApplicableRules: string[] = [];
|
|
131
133
|
let lastNudgePromptKey = "";
|
|
132
134
|
|
|
133
135
|
// Git context (cached, refreshed on hot reload + before_agent_start)
|
|
@@ -307,6 +309,7 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
307
309
|
refreshState: () => refreshState(),
|
|
308
310
|
updateStatus: (ui) => updateStatus(ui),
|
|
309
311
|
getConfig: getActiveConfig,
|
|
312
|
+
getIntentDocs: () => intentDocs,
|
|
310
313
|
});
|
|
311
314
|
|
|
312
315
|
registerTools(pi, {
|
|
@@ -431,6 +434,9 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
431
434
|
nudgeActiveForTask = false;
|
|
432
435
|
lastNudgePromptKey = "";
|
|
433
436
|
sessionStats = { turns: 0, tokensEstimate: 0 };
|
|
437
|
+
editedFilesThisTurn = new Set();
|
|
438
|
+
lastEditedFiles = [];
|
|
439
|
+
lastTurnApplicableRules = [];
|
|
434
440
|
|
|
435
441
|
// Read git context (best-effort, silent on failure)
|
|
436
442
|
gitContext = await readGitContext(ctx.cwd);
|
|
@@ -714,19 +720,14 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
714
720
|
}
|
|
715
721
|
|
|
716
722
|
// Post-work rules check: surface applicable rules for files edited
|
|
717
|
-
// in this turn.
|
|
718
|
-
//
|
|
723
|
+
// in this turn. Tracking is kept silent (no chat notify — user feedback:
|
|
724
|
+
// "spammy") but data is preserved for /why to show last-turn rule context.
|
|
719
725
|
if (editedFilesThisTurn.size > 0) {
|
|
720
|
-
|
|
726
|
+
lastEditedFiles = [...editedFilesThisTurn];
|
|
727
|
+
lastTurnApplicableRules = rulesApplicableToFiles(
|
|
721
728
|
combinedRules(),
|
|
722
|
-
|
|
729
|
+
lastEditedFiles,
|
|
723
730
|
);
|
|
724
|
-
if (applicable.length > 0) {
|
|
725
|
-
ctx.ui.notify(
|
|
726
|
-
`📋 Rules check: edited ${editedFilesThisTurn.size} file(s), ${applicable.length} rule(s) applied:\n • ${applicable.join("\n • ")}`,
|
|
727
|
-
"info",
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
731
|
editedFilesThisTurn = new Set();
|
|
731
732
|
}
|
|
732
733
|
});
|
package/intent.ts
CHANGED
|
@@ -301,3 +301,109 @@ export function loadInlineIntentBodies(intent: IntentDoc[]): IntentInlineDoc[] {
|
|
|
301
301
|
}
|
|
302
302
|
return out;
|
|
303
303
|
}
|
|
304
|
+
|
|
305
|
+
// =============================================================================
|
|
306
|
+
// Intent stats — Claude-memory-style breakdown for docs
|
|
307
|
+
// =============================================================================
|
|
308
|
+
//
|
|
309
|
+
// Shows how docs consume context. Most docs are preview-only (cheap),
|
|
310
|
+
// only `inline: true` docs inject their full body (expensive).
|
|
311
|
+
|
|
312
|
+
export interface IntentDocStat {
|
|
313
|
+
relPath: string;
|
|
314
|
+
kind: "md" | "html";
|
|
315
|
+
title: string;
|
|
316
|
+
tokens: number; // full file tokens (what would be loaded if inline)
|
|
317
|
+
previewTokens: number; // preview tokens (what's actually loaded by default)
|
|
318
|
+
inline: boolean; // true if `inline: true` frontmatter — body is injected
|
|
319
|
+
oversized: boolean;
|
|
320
|
+
phaseNumber?: number;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface IntentStats {
|
|
324
|
+
totalDocs: number;
|
|
325
|
+
totalInlineTokens: number; // tokens from inline: true docs (full body)
|
|
326
|
+
totalPreviewTokens: number; // tokens from preview-only docs (always loaded)
|
|
327
|
+
totalPerTurnTokens: number; // sum of what's actually in system prompt
|
|
328
|
+
inlineDocs: IntentDocStat[];
|
|
329
|
+
previewDocs: IntentDocStat[];
|
|
330
|
+
phaseSpecificDocs: IntentDocStat[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function buildIntentStats(
|
|
334
|
+
docs: IntentDoc[],
|
|
335
|
+
inlineBodies: IntentInlineDoc[],
|
|
336
|
+
): IntentStats {
|
|
337
|
+
const inlineRelPaths = new Set(inlineBodies.map((d) => d.relPath));
|
|
338
|
+
const inlineBodyTokens = new Map(inlineBodies.map((d) => [d.relPath, d.tokens]));
|
|
339
|
+
const stat = (d: IntentDoc): IntentDocStat => ({
|
|
340
|
+
relPath: d.relPath,
|
|
341
|
+
kind: d.kind,
|
|
342
|
+
title: d.title,
|
|
343
|
+
tokens: d.tokens,
|
|
344
|
+
previewTokens: Math.ceil(d.preview.length / 4),
|
|
345
|
+
inline: inlineRelPaths.has(d.relPath),
|
|
346
|
+
oversized: d.oversized,
|
|
347
|
+
phaseNumber: d.phaseNumber,
|
|
348
|
+
});
|
|
349
|
+
const all = docs.map(stat);
|
|
350
|
+
const inlineDocs = all.filter((d) => d.inline);
|
|
351
|
+
const previewDocs = all.filter((d) => !d.inline);
|
|
352
|
+
const phaseSpecificDocs = all.filter((d) => d.phaseNumber != null);
|
|
353
|
+
const totalInlineTokens = inlineDocs.reduce(
|
|
354
|
+
(a, b) => a + (inlineBodyTokens.get(b.relPath) ?? b.tokens),
|
|
355
|
+
0,
|
|
356
|
+
);
|
|
357
|
+
const totalPreviewTokens = previewDocs.reduce((a, b) => a + b.previewTokens, 0);
|
|
358
|
+
return {
|
|
359
|
+
totalDocs: all.length,
|
|
360
|
+
totalInlineTokens,
|
|
361
|
+
totalPreviewTokens,
|
|
362
|
+
totalPerTurnTokens: totalInlineTokens + totalPreviewTokens,
|
|
363
|
+
inlineDocs,
|
|
364
|
+
previewDocs,
|
|
365
|
+
phaseSpecificDocs,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function formatIntentStats(stats: IntentStats): string {
|
|
370
|
+
const lines: string[] = [];
|
|
371
|
+
lines.push(`📚 Docs context stats`);
|
|
372
|
+
lines.push(``);
|
|
373
|
+
lines.push(
|
|
374
|
+
`Loaded: ${stats.totalDocs} doc(s) · ${stats.totalPerTurnTokens} tok every turn`,
|
|
375
|
+
);
|
|
376
|
+
lines.push(
|
|
377
|
+
` (${stats.totalInlineTokens} from inline bodies + ${stats.totalPreviewTokens} from previews)`,
|
|
378
|
+
);
|
|
379
|
+
lines.push(``);
|
|
380
|
+
if (stats.inlineDocs.length > 0) {
|
|
381
|
+
lines.push(`INLINE (full body loaded every turn):`);
|
|
382
|
+
for (const d of stats.inlineDocs) {
|
|
383
|
+
const title = d.title ? ` — "${d.title}"` : "";
|
|
384
|
+
lines.push(` ● ${d.relPath} ${d.tokens} tok${title}`);
|
|
385
|
+
}
|
|
386
|
+
lines.push(``);
|
|
387
|
+
}
|
|
388
|
+
if (stats.previewDocs.length > 0) {
|
|
389
|
+
lines.push(`PREVIEW-ONLY (only title + 180-char preview loaded):`);
|
|
390
|
+
for (const d of stats.previewDocs) {
|
|
391
|
+
const title = d.title ? ` — "${d.title}"` : "";
|
|
392
|
+
const size = d.oversized ? " (oversized)" : "";
|
|
393
|
+
lines.push(` ○ ${d.relPath} ${d.previewTokens} tok preview${title}${size}`);
|
|
394
|
+
}
|
|
395
|
+
lines.push(``);
|
|
396
|
+
}
|
|
397
|
+
if (stats.phaseSpecificDocs.length > 0) {
|
|
398
|
+
lines.push(`PHASE-SPECIFIC (only loaded for matching phase):`);
|
|
399
|
+
for (const d of stats.phaseSpecificDocs) {
|
|
400
|
+
const title = d.title ? ` — "${d.title}"` : "";
|
|
401
|
+
lines.push(` ◐ phase ${d.phaseNumber}: ${d.relPath}${title}`);
|
|
402
|
+
}
|
|
403
|
+
lines.push(``);
|
|
404
|
+
}
|
|
405
|
+
if (stats.totalDocs === 0) {
|
|
406
|
+
lines.push(`No intent docs found in .soly/docs/ or ~/.soly/docs/`);
|
|
407
|
+
}
|
|
408
|
+
return lines.join("\n");
|
|
409
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-soly",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Project management framework for pi-coding-agent. Workflows, planning, multi-question picker, agent switcher, live task list — one npm install, zero config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|