@sage-protocol/pi-adapter 0.2.2 → 0.3.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.
@@ -13,9 +13,92 @@ import {
13
13
  type ExtensionApi = {
14
14
  on: (eventName: string, handler: (event: any, ctx: any) => Promise<any> | any) => void;
15
15
  registerTool: (tool: any) => void;
16
+ registerCommand: (name: string, options: { description: string; handler: (args: string, ctx: any) => Promise<void> | void }) => void;
16
17
  sendMessage: (message: any, options?: any) => void;
17
18
  };
18
19
 
20
+ // ── Sage widget state machine ──
21
+ // Three independent layers that merge into widget lines without clobbering.
22
+ // Baseline persists across turns; suggestions and subagents are transient
23
+ // (cleared at start of each before_agent_start).
24
+
25
+ interface WidgetLayer {
26
+ lines: string[];
27
+ updatedAt: number;
28
+ }
29
+
30
+ class SageWidgetState {
31
+ baseline: WidgetLayer | null = null;
32
+ suggestions: WidgetLayer | null = null;
33
+ subagents: WidgetLayer | null = null;
34
+
35
+ /** Clear transient layers at the start of each turn. */
36
+ clearTransients(): void {
37
+ this.suggestions = null;
38
+ this.subagents = null;
39
+ }
40
+
41
+ /** Replace the baseline (static counts, active libs, exposed skills). */
42
+ setBaseline(text: string): void {
43
+ this.baseline = { lines: text.split('\n').filter(Boolean), updatedAt: Date.now() };
44
+ }
45
+
46
+ /** Add suggestion count/skills to the transient layer. */
47
+ setSuggestions(keys: string[]): void {
48
+ if (keys.length === 0) {
49
+ this.suggestions = null;
50
+ return;
51
+ }
52
+ const names = keys.map(k => k.startsWith('##') ? k.replace(/^##\s*/, '') : k);
53
+ const line = names.length === 1
54
+ ? `↳ suggested: ${names[0]}`
55
+ : `↳ ${names.length} suggested: ${names.slice(0, 6).join(', ')}${names.length > 6 ? ` +${names.length - 6} more` : ''}`;
56
+ this.suggestions = { lines: [line], updatedAt: Date.now() };
57
+ }
58
+
59
+ /** Add resolved subagent skill count to the transient layer. */
60
+ setSubagents(keys: string[]): void {
61
+ if (keys.length === 0) {
62
+ this.subagents = null;
63
+ return;
64
+ }
65
+ this.subagents = {
66
+ lines: [`↳ ${keys.length} subagent: ${keys.slice(0, 4).join(', ')}${keys.length > 4 ? ` +${keys.length - 4} more` : ''}`],
67
+ updatedAt: Date.now(),
68
+ };
69
+ }
70
+
71
+ /** Merge all layers into widget lines. */
72
+ render(): string[] {
73
+ const output: string[] = [];
74
+ if (this.baseline) output.push(...this.baseline.lines);
75
+ if (this.suggestions) output.push(...this.suggestions.lines);
76
+ if (this.subagents) output.push(...this.subagents.lines);
77
+ return output;
78
+ }
79
+
80
+ /** Push rendered output to the Pi widget. */
81
+ updateWidget(ui: any): void {
82
+ if (!ui?.setWidget) return;
83
+ const lines = this.render();
84
+ if (lines.length === 0) return;
85
+ ui.setWidget('sage', lines);
86
+ }
87
+ }
88
+
89
+ /** Extract skill keys from Sage suggestion output. */
90
+ function parseSuggestionKeys(text: string): string[] {
91
+ const keys: string[] = [];
92
+ for (const line of text.split('\n')) {
93
+ const trimmed = line.trim();
94
+ if (!trimmed.startsWith('-')) continue;
95
+ // Match patterns like: "- code-audit" or "- code-audit (code-audit)" or "- code-audit - desc"
96
+ const m = trimmed.match(/^-\s+(\S+?)(?:\s+\(|$|\s+-)/);
97
+ if (m) keys.push(m[1]);
98
+ }
99
+ return keys;
100
+ }
101
+
19
102
  function parseBoolean(value: string | undefined, fallback: boolean): boolean {
20
103
  if (value === undefined) return fallback;
21
104
  const normalized = value.trim().toLowerCase();
@@ -226,6 +309,61 @@ function extractUsage(message: any): { input?: string; output?: string } {
226
309
  };
227
310
  }
228
311
 
312
+ function normalizeSkillPath(value: unknown): string | null {
313
+ if (typeof value !== 'string') return null;
314
+ const trimmed = value.trim();
315
+ if (!trimmed) return null;
316
+ return trimmed.replace(/^file:\/\//, '').replace(/\\+/g, '/').replace(/\/+/g, '/').toLowerCase();
317
+ }
318
+
319
+ function skillNameFromPath(path: string): string | null {
320
+ const normalized = normalizeSkillPath(path);
321
+ if (!normalized || !normalized.endsWith('/skill.md')) return null;
322
+ const parts = normalized.split('/').filter(Boolean);
323
+ if (parts.length < 2) return null;
324
+ return parts[parts.length - 2] ?? null;
325
+ }
326
+
327
+ function extractSkillCommandName(text: unknown): string | null {
328
+ if (typeof text !== 'string') return null;
329
+ const match = text.trim().match(/^\/skill:([a-z0-9][a-z0-9-]*)\b/i);
330
+ return match?.[1]?.toLowerCase() ?? null;
331
+ }
332
+
333
+ function extractReadPath(input: unknown): string | null {
334
+ if (!input || typeof input !== 'object') return null;
335
+ const obj = input as Record<string, unknown>;
336
+ const raw = obj.file_path ?? obj.path;
337
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : null;
338
+ }
339
+
340
+ function refreshSkillPathIndex(skills: unknown, skillPathToName: Map<string, string>): void {
341
+ if (!Array.isArray(skills)) return;
342
+
343
+ for (const item of skills) {
344
+ if (!item || typeof item !== 'object') continue;
345
+ const skill = item as Record<string, unknown>;
346
+ const name = typeof skill.name === 'string' && skill.name.trim()
347
+ ? skill.name.trim().toLowerCase()
348
+ : null;
349
+ if (!name) continue;
350
+
351
+ for (const key of ['filePath', 'file_path', 'path', 'location']) {
352
+ const normalized = normalizeSkillPath(skill[key]);
353
+ if (normalized) skillPathToName.set(normalized, name);
354
+ }
355
+
356
+ const baseDir = normalizeSkillPath(skill.baseDir ?? skill.dir ?? skill.directory);
357
+ if (baseDir) skillPathToName.set(`${baseDir}/skill.md`, name);
358
+ }
359
+ }
360
+
361
+ function skillNameFromReadEvent(event: any, skillPathToName: Map<string, string>): string | null {
362
+ const path = extractReadPath(event?.input ?? event?.args);
363
+ const normalized = normalizeSkillPath(path);
364
+ if (!normalized || !normalized.endsWith('/skill.md')) return null;
365
+ return skillPathToName.get(normalized) ?? skillNameFromPath(normalized);
366
+ }
229
367
 
230
368
  /**
231
369
  * Behavior-aware suggestion enrichment (Option A).
@@ -406,6 +544,7 @@ function formatBehaviorSuggestion(behavior: BehaviorMatch, prompt: string): stri
406
544
  export default function sagePiAdapterExtension(pi: ExtensionApi): void {
407
545
  const showHookMessages = parseBoolean(process.env.SAGE_SHOW_HOOK_MESSAGES, false);
408
546
  const allowBashSageCli = parseBoolean(process.env.SAGE_ALLOW_BASH_CLI, true);
547
+ const enableBehaviorEnrich = parseBoolean(process.env.SAGE_BEHAVIOR_ENRICH, false);
409
548
  const config: SageP2Config = {
410
549
  sageBin: process.env.SAGE_BIN ?? 'sage',
411
550
  sageProfile: process.env.SAGE_PROFILE,
@@ -454,12 +593,12 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
454
593
  label: 'Sage: Behavior',
455
594
  promptSnippet: 'Resolve and execute a Sage behavior plan (multi-skill workflow chain)',
456
595
  description:
457
- 'Resolve a behavior key into a multi-skill execution plan and render it as a structured ' +
458
- 'controller prompt. Behaviors chain multiple skills into a linear workflow — e.g., ' +
459
- 'entry-plan-review chains: sage → reverse-interviewing → pre-mortem → ceo-plan-review → ' +
460
- 'plan-eng-review → openprose → confidence-calibration. ' +
596
+ 'Resolve and execute a Sage behavior: a multi-skill workflow whose resolved steps are ' +
597
+ 'coordinated by the Pi behavior executor. Behaviors chain multiple skills into a linear ' +
598
+ 'workflow — e.g., entry-plan-review chains: sage → reverse-interviewing → pre-mortem → ' +
599
+ 'ceo-plan-review → plan-eng-review → openprose → confidence-calibration. ' +
461
600
  'Call with key=\'entry-plan-review\' and task=\'Review my implementation plan\'. ' +
462
- 'The tool returns a structured execution plan for the agent to follow step by step.',
601
+ 'The tool returns the executor payload/result for the resolved skill chain.',
463
602
  parameters: {
464
603
  type: 'object' as const,
465
604
  properties: {
@@ -525,6 +664,38 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
525
664
  },
526
665
  });
527
666
 
667
+ // Command: manually inspect Sage context injection
668
+ pi.registerCommand('sage-context', {
669
+ description: 'Show the Sage session context and loaded skills',
670
+ handler: async (_args: string, ctx: any) => {
671
+ const sessionId = getSessionId(ctx);
672
+ let output = '';
673
+
674
+ try {
675
+ const context = await hookRuntime.getSessionContext(sessionId);
676
+ if (context) {
677
+ output += '## Injected Sage Context (hidden from UI)\n```\n' +
678
+ (context.length > 2000 ? context.slice(0, 2000) + '\n... (truncated)' : context) +
679
+ '\n```\n\n';
680
+ }
681
+ } catch {
682
+ output += '(sage skill context --format pi failed)\n\n';
683
+ }
684
+
685
+ try {
686
+ const summary = await hookRuntime.getSkillVisibilitySummary();
687
+ if (summary) {
688
+ output += '## Visible Summary\n' + summary + '\n\n';
689
+ }
690
+ } catch {
691
+ output += '(library summary failed)\n\n';
692
+ }
693
+
694
+ output += 'Tip: set SAGE_SHOW_HOOK_MESSAGES=1 to see context in the UI.';
695
+ ctx.ui.notify(output, 'info');
696
+ },
697
+ });
698
+
528
699
  // ---------------------------------------------------------------------------
529
700
  // System prompt injection: Sage-first routing for Pi sessions
530
701
  // ---------------------------------------------------------------------------
@@ -534,8 +705,8 @@ export default function sagePiAdapterExtension(pi: ExtensionApi): void {
534
705
  // Tier 2: Full usage patterns injected once at session_start (below).
535
706
  // Tier 3: Complete CLI reference available on-demand via sage_status({}).
536
707
  //
537
- // This per-turn string is a minimal guardrail that complements tool-level docs.
538
- // It does NOT repeat usage patterns — those are in Tier 2 (session_start).
708
+ // This per-turn string is intentionally small. Do not prepend the full
709
+ // `sage skill context` output here; that belongs in Tier 2 only.
539
710
  const SAGE_PER_TURN_GUARDRAIL = `
540
711
  [Sage Tools - Internet Access]
541
712
  You have sage_search, sage_execute, and sage_status tools for internet and external service access.
@@ -544,9 +715,9 @@ Sage internal tools are available immediately. Only external MCP servers need li
544
715
  `.trim();
545
716
 
546
717
  // Tier 2: Full Sage protocol context injected ONCE at session_start.
547
- // Includes skill trigger workflow, external research patterns, hub management,
548
- // library/DAO/bounty recipes, and best practices — everything the model needs
549
- // to use Sage effectively, but only needs to see once per session.
718
+ // Prefer the dynamic CLI context because it is generated from the canonical
719
+ // Sage runtime templates. Keep the static block as a fallback only; appending
720
+ // both creates duplicate routing instructions in every Pi session.
550
721
  const SAGE_SESSION_CONTEXT = `
551
722
  ## Sage MCP + CLI Routing
552
723
 
@@ -587,30 +758,77 @@ Use these tools proactively when the user asks about current events, needs docum
587
758
 
588
759
  let lastPrompt = '';
589
760
  let lastSuggestionAt = 0;
761
+ const skillPathToName = new Map<string, string>();
762
+ const pendingInputSkillUses = new Set<string>();
763
+ let usedSkillsThisTurn = new Set<string>();
764
+ let pendingReadToolSkills = new Map<string, string>();
765
+ let currentPromptPreview = '';
766
+ let toolErrorSeenThisTurn = false;
767
+ const autoRecordSkillOutcome = parseBoolean(process.env.SAGE_AUTO_SKILL_OUTCOME, true);
768
+
769
+ // Sage widget state — baseline + per-turn transients
770
+ const sageWidget = new SageWidgetState();
771
+
772
+ const rememberSkillUse = (skillName: string | null | undefined): void => {
773
+ const normalized = typeof skillName === 'string' ? skillName.trim().toLowerCase() : '';
774
+ if (normalized) usedSkillsThisTurn.add(normalized);
775
+ };
776
+
590
777
  pi.on('session_start', async (_event, ctx) => {
591
778
  const sessionId = getSessionId(ctx);
592
779
 
593
- // Tier 2: Inject full Sage protocol context once at session start.
594
- // This includes skill trigger workflow, external tool patterns, recipes,
595
- // and best practices — everything the model needs but only sees once.
780
+ // Inject full Sage protocol context once at session start.
596
781
  let fullContext = SAGE_SESSION_CONTEXT;
597
782
  try {
598
783
  const dynamicContext = await hookRuntime.getSessionContext(sessionId);
599
784
  if (dynamicContext.trim()) {
600
- fullContext = `${dynamicContext.trim()}\n\n${SAGE_SESSION_CONTEXT}`;
785
+ fullContext = dynamicContext.trim();
601
786
  }
602
787
  } catch {
603
- // Keep static SAGE_SESSION_CONTEXT as fallback
788
+ // Keep static SAGE_SESSION_CONTEXT as fallback.
604
789
  }
605
790
 
606
791
  pi.sendMessage(
607
792
  {
608
793
  customType: 'sage-session-context',
609
794
  content: fullContext,
610
- display: showHookMessages,
795
+ display: false,
611
796
  },
612
797
  { deliverAs: 'nextTurn' }
613
798
  );
799
+
800
+ // Update persistent skill widget via state machine.
801
+ // Fetch baseline once, render, and update widget.
802
+ try {
803
+ const summary = await hookRuntime.getSkillVisibilitySummary();
804
+ if (summary) {
805
+ sageWidget.setBaseline(summary);
806
+ sageWidget.updateWidget(ctx.ui);
807
+ }
808
+ } catch {
809
+ // Non-blocking.
810
+ }
811
+
812
+ // Fallback: hook message for non-TUI modes
813
+ if (!ctx.ui?.setWidget) {
814
+ try {
815
+ const summary = await hookRuntime.getSkillVisibilitySummary();
816
+ if (summary) {
817
+ pi.sendMessage(
818
+ { customType: 'sage-loaded', content: summary, display: true },
819
+ { deliverAs: 'nextTurn' }
820
+ );
821
+ }
822
+ } catch {
823
+ // Non-blocking.
824
+ }
825
+ }
826
+ });
827
+
828
+ pi.on('input', async (event) => {
829
+ const skillName = extractSkillCommandName(event?.text);
830
+ if (skillName) pendingInputSkillUses.add(skillName);
831
+ return { action: 'continue' };
614
832
  });
615
833
 
616
834
  pi.on('before_agent_start', async (event, ctx) => {
@@ -619,36 +837,28 @@ Use these tools proactively when the user asks about current events, needs docum
619
837
  const continuationPrompt = isContinuationPrompt(prompt);
620
838
 
621
839
  const sessionId = getSessionId(ctx);
840
+ refreshSkillPathIndex(event?.systemPromptOptions?.skills, skillPathToName);
841
+ usedSkillsThisTurn = new Set(pendingInputSkillUses);
842
+ pendingInputSkillUses.clear();
843
+ pendingReadToolSkills = new Map<string, string>();
844
+ currentPromptPreview = prompt.slice(0, 240);
845
+ toolErrorSeenThisTurn = false;
846
+
847
+ // Clear per-turn transient widget layers
848
+ sageWidget.clearTransients();
849
+
622
850
  await hookRuntime.capturePrompt(prompt, sessionId);
623
851
 
624
852
  // --- System prompt injection (EVERY turn) ---
625
853
  // Pi resets to _baseSystemPrompt if result.systemPrompt is undefined,
626
- // so we must always return the modified prompt to keep Sage docs active.
854
+ // so we must always return the modified prompt to keep the compact Sage
855
+ // guardrail active. Keep this block small: the full Sage context is sent
856
+ // once during session_start.
627
857
  let modifiedSystemPrompt: string | undefined;
628
858
  if (injectToolDocs) {
629
859
  const currentSystemPrompt = typeof event?.systemPrompt === 'string' ? event.systemPrompt : '';
630
860
  if (currentSystemPrompt) {
631
861
  let docs = SAGE_PER_TURN_GUARDRAIL;
632
- try {
633
- const sessionContext = await hookRuntime.getSessionContext(sessionId);
634
- if (sessionContext.trim()) {
635
- docs = `${sessionContext.trim()}\n\n${SAGE_PER_TURN_GUARDRAIL}`;
636
- }
637
- } catch {
638
- // Session-context injection is best-effort — never block the turn.
639
- }
640
-
641
- // Dynamic identity context: wallet, libraries, skill counts.
642
- // Cached for 60s so the CLI cost is negligible per-turn.
643
- try {
644
- const identity = await hookRuntime.getIdentityContext();
645
- if (identity) {
646
- docs = `${identity}\n\n${docs}`;
647
- }
648
- } catch {
649
- // Identity injection is best-effort — never block the turn.
650
- }
651
-
652
862
  modifiedSystemPrompt = currentSystemPrompt + '\n\n' + docs;
653
863
  }
654
864
  }
@@ -706,6 +916,9 @@ Use these tools proactively when the user asks about current events, needs docum
706
916
  skillBlocks.push(`\n--- Sage Skill: ${name} ---\n${content}\n--- End Skill: ${name} ---`);
707
917
  }
708
918
  modifiedSystemPrompt += '\n\n[Auto-resolved Sage Skills]\n' + skillBlocks.join('\n');
919
+
920
+ // Track for widget — rendered at end of before_agent_start
921
+ sageWidget.setSubagents([...resolved.keys()]);
709
922
  }
710
923
  } catch {
711
924
  // Non-blocking — subagent proceeds without injected skills
@@ -726,11 +939,8 @@ Use these tools proactively when the user asks about current events, needs docum
726
939
 
727
940
  let suggestions = await hookRuntime.suggestSkill(prompt, sessionId);
728
941
 
729
- // Option A: Enrich suggestions with behavior chain info when a
730
- // behavior entrypoint matches. This gives the model both options
731
- // (full chain via sage_behavior OR individual skill via use_skill)
732
- // and lists all steps for false-positive assessment.
733
- if (suggestions.trim()) {
942
+ // Behavior enrichment — opt-in via SAGE_BEHAVIOR_ENRICH=1
943
+ if (suggestions.trim() && enableBehaviorEnrich) {
734
944
  try {
735
945
  suggestions = await enrichSuggestionsWithBehaviors(suggestions, prompt, bridge);
736
946
  } catch {
@@ -744,9 +954,17 @@ Use these tools proactively when the user asks about current events, needs docum
744
954
  content: suggestions,
745
955
  display: showHookMessages,
746
956
  };
957
+
958
+ // Update widget with suggestion skill names
959
+ const suggestedKeys = parseSuggestionKeys(suggestions);
960
+ sageWidget.setSuggestions(suggestedKeys);
747
961
  }
748
962
  }
749
963
 
964
+ // Always push current widget state after processing suggestions.
965
+ // Transients may have been cleared or set; render them now.
966
+ sageWidget.updateWidget(ctx.ui);
967
+
750
968
  // Return combined result if either system prompt or message changed
751
969
  if (modifiedSystemPrompt || suggestionMessage) {
752
970
  const result: Record<string, unknown> = {};
@@ -756,6 +974,28 @@ Use these tools proactively when the user asks about current events, needs docum
756
974
  }
757
975
  });
758
976
 
977
+ pi.on('agent_end', async (_event, ctx) => {
978
+ const usedSkills = [...usedSkillsThisTurn];
979
+ if (usedSkills.length === 0) return;
980
+
981
+ const sessionId = getSessionId(ctx);
982
+ await hookRuntime.recordSuggestionUse(usedSkills, sessionId);
983
+
984
+ if (autoRecordSkillOutcome) {
985
+ const status = toolErrorSeenThisTurn ? 'failed' : 'passed';
986
+ await Promise.allSettled(
987
+ usedSkills.map((skillName) =>
988
+ hookRuntime.recordExecutionOutcome(skillName, status, currentPromptPreview, sessionId)
989
+ )
990
+ );
991
+ }
992
+
993
+ usedSkillsThisTurn = new Set<string>();
994
+ pendingReadToolSkills = new Map<string, string>();
995
+ currentPromptPreview = '';
996
+ toolErrorSeenThisTurn = false;
997
+ });
998
+
759
999
  pi.on('message_end', async (event, ctx) => {
760
1000
  const message = event?.message;
761
1001
  if (!message || typeof message !== 'object') return;
@@ -773,7 +1013,15 @@ Use these tools proactively when the user asks about current events, needs docum
773
1013
 
774
1014
  pi.on('tool_call', async (event, ctx) => {
775
1015
  const toolName = typeof event?.toolName === 'string' ? event.toolName : '';
776
- if (!toolName || toolName.startsWith('sage_')) return;
1016
+ if (!toolName) return;
1017
+
1018
+ if (toolName === 'read') {
1019
+ const skillName = skillNameFromReadEvent(event, skillPathToName);
1020
+ const toolCallId = typeof event?.toolCallId === 'string' ? event.toolCallId : '';
1021
+ if (skillName && toolCallId) pendingReadToolSkills.set(toolCallId, skillName);
1022
+ }
1023
+
1024
+ if (toolName.startsWith('sage_')) return;
777
1025
 
778
1026
  const toolInput =
779
1027
  event?.input && typeof event.input === 'object'
@@ -855,7 +1103,18 @@ Use these tools proactively when the user asks about current events, needs docum
855
1103
 
856
1104
  pi.on('tool_result', async (event, ctx) => {
857
1105
  const toolName = typeof event?.toolName === 'string' ? event.toolName : '';
858
- if (!toolName || toolName.startsWith('sage_')) return;
1106
+ if (!toolName) return;
1107
+
1108
+ if (Boolean(event?.isError)) toolErrorSeenThisTurn = true;
1109
+
1110
+ if (toolName === 'read' && !Boolean(event?.isError)) {
1111
+ const toolCallId = typeof event?.toolCallId === 'string' ? event.toolCallId : '';
1112
+ const skillName = skillNameFromReadEvent(event, skillPathToName) ?? pendingReadToolSkills.get(toolCallId);
1113
+ rememberSkillUse(skillName);
1114
+ if (toolCallId) pendingReadToolSkills.delete(toolCallId);
1115
+ }
1116
+
1117
+ if (toolName.startsWith('sage_')) return;
859
1118
 
860
1119
  const toolInput =
861
1120
  event?.input && typeof event.input === 'object'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/pi-adapter",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Sage Protocol MCP integration for pi coding agent",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  "@sinclair/typebox": "^0.34.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@mariozechner/pi-coding-agent": ">=0.61.0 <0.65.0"
41
+ "@earendil-works/pi-coding-agent": ">=0.64.0 <0.80.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "^24.0.0",
@@ -50,6 +50,11 @@ sage_status({})
50
50
 
51
51
  Returns bridge health, wallet connection, network, and session info.
52
52
 
53
+ If `sage_status` reports a degraded or unavailable bridge, do not keep retrying
54
+ silently. Tell the user the Sage MCP bridge is unavailable and suggest
55
+ restarting Pi/Sage bridge or verifying `sage mcp start`. Only suggest
56
+ `sage daemon start` when the reported failure is specifically daemon reachability.
57
+
53
58
  ---
54
59
 
55
60
  ## Behavior Loop
@@ -85,13 +90,40 @@ sage_search({ domain: "rlm", action: "stats" })
85
90
  **5. Earn & Collaborate** — Publish, vote, tip, bounty.
86
91
 
87
92
  ```bash
88
- sage library push my-library
93
+ sage library push my-library --bump patch
89
94
  sage governance proposals vote 1 for
90
95
  sage bounties list
91
96
  ```
92
97
 
93
98
  ---
94
99
 
100
+ ## Choose a Sage distribution surface by intent
101
+
102
+ Do not collapse every write path into a generic idea of "publish." Choose the smallest surface that matches the actual goal:
103
+
104
+ - **P2P** — sync trusted machines/agents privately.
105
+ - **Shared library** — collaborate privately with explicit members/invites.
106
+ - **Personal cloud** — host a creator-controlled canonical library; it starts private by default.
107
+ - **Public personal cloud** — make a hosted library publicly discoverable only when the user explicitly asks, with `sage library visibility <library-id> public`.
108
+ - **Marketplace** — sell a polished public personal library as a packaged capability.
109
+ - **DAO promotion** — make a library canonical for a group/community and unlock curation, legitimacy, on-chain reputation, tips, bounties, and community-driven improvement.
110
+
111
+ **Governance chooser**
112
+ - **personal DAO** — fastest operator-controlled canon
113
+ - **team DAO** — trusted shared stewardship
114
+ - **community DAO** — strongest public curation and legitimacy
115
+
116
+ Pi routing rules:
117
+ - If the user says **direct share** or **share privately**, think **shared library** first.
118
+ - If the goal is trusted fleet propagation, think **P2P**, not governance.
119
+ - If the goal is creator-controlled hosting, think **personal cloud** and remember it is private by default.
120
+ - If the goal is public web/discovery access, confirm explicit intent before making a library public.
121
+ - If the goal is monetization, think **marketplace**.
122
+ - If the goal is public canon, stronger provenance, future bounties, and repeated community improvement, think **DAO promotion**.
123
+ - If one surface is clearly implied, proactively suggest it instead of waiting for the user to name the exact Sage term.
124
+ - Install/expose is local harness visibility only; do not treat it as publishing.
125
+ - Public feeds/search should only advertise anonymously readable prompts and skills; keep owner-only, unpinned, expired, or otherwise non-openable CIDs out of public discovery until republished or made public.
126
+
95
127
  ## When to Trigger
96
128
 
97
129
  Proactively search Sage when:
@@ -131,7 +163,7 @@ Recommended `.pi/settings.json` shape in a local monorepo:
131
163
  "-../.agents/skills/frontend-design/SKILL.md",
132
164
  "-~/.pi/agent/skills/prompt-builder/SKILL.md",
133
165
  "-~/.pi/agent/skills/sage/SKILL.md",
134
- "-~/.pi/agent/skills/sage-codebase/SKILL.md"
166
+ "-~/.pi/agent/skills/sage-workflow/SKILL.md"
135
167
  ],
136
168
  "enableSkillCommands": true
137
169
  }
@@ -190,8 +222,9 @@ sage library list
190
222
  sage library create <name>
191
223
  sage library skill add <path> -l <library>
192
224
  sage library prompt add <name> -l <library> --file <path>
193
- sage library push <library>
225
+ sage library push <library> --bump patch
194
226
  sage library promote <library> --dao 0x... --collection default --yes
227
+ # Use `major`, `minor`, or `patch` for `--bump`; exact SemVer and `status --across` are not current CLI commands.
195
228
  sage library discover
196
229
  sage library sync
197
230
  ```
@@ -237,7 +270,9 @@ sage social post "message"
237
270
  ```bash
238
271
  sage wallet balance
239
272
  sage wallet current
240
- sage wallet connect privy
273
+ sage wallet create sage-wallet
274
+ sage wallet connect ows -n sage-wallet
275
+ sage wallet connect privy --device-code
241
276
  sage wallet delegate
242
277
  sage delegation create --delegation-profile voter
243
278
  sage delegation list
@@ -247,7 +282,9 @@ sage agent register
247
282
  sage agent quickstart
248
283
  ```
249
284
 
250
- `sage wallet delegate` is ERC20Votes self-delegation for governance voting power. `sage delegation ...` is ERC-7710 scoped agent delegation management.
285
+ Preserve the user's already-working wallet/session preference when one exists. The examples above show common direct-wallet and provider-session paths, not a requirement to force one provider.
286
+
287
+ `sage wallet delegate` is ERC20Votes self-delegation for governance voting power. `sage delegation ...` is scoped agent delegation management.
251
288
 
252
289
  ### RLM Capture
253
290
 
@@ -274,6 +311,7 @@ sage mcp call <tool> --args '{...}'
274
311
  ## Common Pitfalls
275
312
 
276
313
  - **Hook mode vs query mode**: `sage suggest hook skill` reads the `PROMPT` environment variable or stdin — don't pass the prompt as a positional argument.
314
+ - **Bridge vs daemon wording**: Pi primarily talks to Sage through the MCP bridge. When `sage_status` shows bridge degradation, say "Sage MCP bridge unavailable" and verify `sage mcp start`; don't default to daemon-start advice unless the daemon is the reported failure.
277
315
  - **`external/search` requires `tool_name`**: Always include `tool_name` in params when searching via external domain (e.g., `tool_name: "web_search"`).
278
316
  - **Domain routing**: Each domain has its own set of actions. Use `sage_search({ domain: "help", action: "list" })` to discover available actions.
279
317
  - **Legacy commands removed**: Do not use `sage dao ...`, `sage proposals ...`, or `sage ipfs ...` — these are replaced by `sage governance dao ...`, `sage governance proposals ...`, and `sage config ipfs ...`.