pi-soly 1.12.2 → 1.13.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/config.ts CHANGED
@@ -45,6 +45,13 @@ export interface SolyConfig {
45
45
  * user (ask "ready to implement, or discuss first?") before writing code.
46
46
  * Default on. */
47
47
  confirmBeforeCode: boolean;
48
+ /** Inject a compact list of rules applicable to the file being
49
+ * edited/written, appended to the tool's result. Forces the LLM to
50
+ * confirm in the next message which rules were applied — closes the
51
+ * "rules read at start of turn, forgotten by edit time" gap.
52
+ * Default on. Disable for verbose rule sets that would otherwise
53
+ * flood the per-edit context. */
54
+ preActionRuleReminder: boolean;
48
55
  };
49
56
  display: {
50
57
  /** Always show the recommended (⭐) option as the first row. */
@@ -125,6 +132,7 @@ export const DEFAULT_CONFIG: SolyConfig = {
125
132
  nudgeNotify: false,
126
133
  toolHints: true,
127
134
  confirmBeforeCode: true,
135
+ preActionRuleReminder: true,
128
136
  },
129
137
  display: {
130
138
  defaultRecommendedFirst: true,
@@ -216,6 +224,8 @@ function deepMerge(base: SolyConfig, over: RawConfig): SolyConfig {
216
224
  merged.agent.toolHints = over.agent.toolHints;
217
225
  if (typeof over.agent.confirmBeforeCode === "boolean")
218
226
  merged.agent.confirmBeforeCode = over.agent.confirmBeforeCode;
227
+ if (typeof over.agent.preActionRuleReminder === "boolean")
228
+ merged.agent.preActionRuleReminder = over.agent.preActionRuleReminder;
219
229
  }
220
230
  if (over.display) {
221
231
  if (typeof over.display.defaultRecommendedFirst === "boolean")
@@ -0,0 +1,54 @@
1
+ // =============================================================================
2
+ // context-manager.ts — single owner of pi's `context` channel
3
+ // =============================================================================
4
+ //
5
+ // pi fires a `context` event before each LLM call; a handler may return
6
+ // `{ messages }` to replace what the model sees for that one call. Only ONE
7
+ // owner should do this, or rewriters fight. soly takes ownership here and
8
+ // exposes a single pluggable rewriter slot — workflows (e.g. `/verify`'s
9
+ // fresh-context mode) install/remove a rewriter through this manager instead
10
+ // of each registering their own `context` handler.
11
+ //
12
+ // The rewriter must return a subset/reordering of the SAME message objects it
13
+ // is given (never fabricate messages — their full shape is owned by pi). The
14
+ // handler is fail-safe: a throwing rewriter leaves the turn untouched.
15
+ // =============================================================================
16
+
17
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
+
19
+ /** Minimal message shape the manager needs (pi's AgentMessage is a superset). */
20
+ export type AgentMessageLike = { role: string };
21
+
22
+ /** Transforms the message list for the next LLM call. Returns real messages. */
23
+ export type ContextRewriter = (messages: AgentMessageLike[]) => AgentMessageLike[];
24
+
25
+ export type ContextManager = {
26
+ /** Install a rewriter, or pass null to restore pass-through. */
27
+ setRewriter(rewriter: ContextRewriter | null): void;
28
+ /** Whether a rewriter is currently active. */
29
+ hasRewriter(): boolean;
30
+ };
31
+
32
+ /** Create the manager and register the single `context` handler. */
33
+ export function createContextManager(pi: ExtensionAPI): ContextManager {
34
+ let rewriter: ContextRewriter | null = null;
35
+
36
+ pi.on("context", (event) => {
37
+ if (!rewriter) return;
38
+ try {
39
+ const messages = rewriter(event.messages) as typeof event.messages;
40
+ return { messages };
41
+ } catch {
42
+ return; // never break a turn over a rewriter bug
43
+ }
44
+ });
45
+
46
+ return {
47
+ setRewriter(next): void {
48
+ rewriter = next;
49
+ },
50
+ hasRewriter(): boolean {
51
+ return rewriter !== null;
52
+ },
53
+ };
54
+ }
package/core.ts CHANGED
@@ -571,6 +571,68 @@ ${blocks.join("\n\n---\n\n")}${skippedNote}
571
571
  return { section, loaded: applicable.map(ruleKey), interactive };
572
572
  }
573
573
 
574
+ // ============================================================================
575
+ // Per-file rule lookup (used by pre-action reminder in tool_result hook)
576
+ // ============================================================================
577
+ //
578
+ // Returns the subset of `rules` that apply to a single file path. Mirrors the
579
+ // glob logic in `buildRulesSection` (always/no-globs = universal; otherwise
580
+ // match `filePath` against `meta.globs`). Excludes disabled rules. Use this
581
+ // to feed the pre-action reminder — LLM should see applicable rules AT THE
582
+ // MOMENT of edit, not just at the start of the turn (where they decay).
583
+ export function getApplicableRulesForFile(
584
+ filePath: string,
585
+ rules: RuleFile[],
586
+ ): RuleFile[] {
587
+ return rules.filter((r) => {
588
+ if (!r.enabled) return false;
589
+ if (r.meta.always === true) return true;
590
+ const globs = r.meta.globs;
591
+ if (!globs || globs.length === 0) return true;
592
+ return globs.some((g) => matchesGlob(filePath, g));
593
+ });
594
+ }
595
+
596
+ // Formats a compact reminder block for the LLM to see right after editing
597
+ // `filePath`. Sorted by priority (high → medium → low → undefined), capped
598
+ // at `cap` rules (default 3) so the LLM isn't flooded. Returns "" when no
599
+ // rules apply — caller should treat that as "don't inject anything".
600
+ export function formatRuleReminder(
601
+ rules: RuleFile[],
602
+ filePath: string,
603
+ cap = 3,
604
+ ): string {
605
+ if (rules.length === 0) return "";
606
+
607
+ const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
608
+ const sorted = [...rules].sort((a, b) => {
609
+ const pa = priorityOrder[a.meta.priority ?? ""] ?? 1;
610
+ const pb = priorityOrder[b.meta.priority ?? ""] ?? 1;
611
+ if (pa !== pb) return pa - pb;
612
+ return a.relPath.localeCompare(b.relPath);
613
+ });
614
+
615
+ const shown = sorted.slice(0, cap);
616
+ const remaining = rules.length - shown.length;
617
+
618
+ const lines: string[] = [];
619
+ lines.push(`\n\n---\n\n📋 **Applicable rules for \`${filePath}\`:**\n`);
620
+ shown.forEach((r, i) => {
621
+ const desc = r.meta.description ? ` — ${r.meta.description}` : "";
622
+ lines.push(`${i + 1}. [\`${r.relPath}\`]${desc}`);
623
+ });
624
+ if (remaining > 0) {
625
+ lines.push(
626
+ `\n_…and ${remaining} more — use \`/why ${filePath}\` to see all._`,
627
+ );
628
+ }
629
+ lines.push(
630
+ `\nIn your next message, briefly confirm which were applied (or note why not).`,
631
+ );
632
+
633
+ return lines.join("\n");
634
+ }
635
+
574
636
  // ============================================================================
575
637
  // Phase-scoped rules loader
576
638
  // ============================================================================
package/index.ts CHANGED
@@ -32,7 +32,9 @@ import {
32
32
  CONTEXT_WINDOW_TOKENS,
33
33
  DEFAULT_PROGRESS,
34
34
  extractFilePathsFromPrompt,
35
+ formatRuleReminder,
35
36
  formatTok,
37
+ getApplicableRulesForFile,
36
38
  loadAllRules,
37
39
  loadPhaseRules,
38
40
  loadProjectState,
@@ -834,6 +836,29 @@ export default function solyExtension(pi: ExtensionAPI) {
834
836
  }
835
837
  });
836
838
 
839
+ // ============================================================================
840
+ // tool_result: inject a compact reminder of rules applicable to the file
841
+ // just edited/written, so the LLM sees them AT THE MOMENT of action (not
842
+ // 30 turns later when they've decayed in attention). The LLM is asked to
843
+ // confirm in its next message which rules were applied — closing the gap
844
+ // where rules loaded at the start of a turn are forgotten by the time the
845
+ // agent actually edits something.
846
+ // ============================================================================
847
+ pi.on("tool_result", async (event, _ctx) => {
848
+ if (!getActiveConfig().agent.preActionRuleReminder) return;
849
+ if (event.toolName !== "edit" && event.toolName !== "write") return;
850
+ if (event.isError) return; // tool failed — don't pile on
851
+ const input = event.input as { path?: string };
852
+ const filePath = input?.path;
853
+ if (!filePath) return;
854
+ const applicable = getApplicableRulesForFile(filePath, combinedRules());
855
+ const reminder = formatRuleReminder(applicable, filePath);
856
+ if (!reminder) return;
857
+ return {
858
+ content: [...event.content, { type: "text" as const, text: reminder }],
859
+ };
860
+ });
861
+
837
862
  // ============================================================================
838
863
  // Chrome: working-indicator telemetry lifecycle + reactive data refresh
839
864
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "1.12.2",
3
+ "version": "1.13.1",
4
4
  "description": "Project management + workflow framework for pi-coding-agent. Plans, state, MANDATORY rules, self-review, multi-question picker, native footer + welcome chrome — one npm install, zero config. The LLM drives execution and delegates to a worker subagent when available.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -30,6 +30,7 @@
30
30
  "README.md",
31
31
  "commands.ts",
32
32
  "config.ts",
33
+ "context-manager.ts",
33
34
  "core.ts",
34
35
  "docs.ts",
35
36
  "env.ts",