pi-soly 1.9.2 → 1.10.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/ask/index.ts CHANGED
@@ -165,6 +165,11 @@ export default function piAskExtension(pi: ExtensionAPI) {
165
165
  if (!ctx.hasUI) return undefined;
166
166
  return (await ctx.ui.input(req.title, req.placeholder)) ?? undefined;
167
167
  },
168
+ // Bridge for the `n` note dialog. Reuses the same text-input UI.
169
+ onRequestNote: async (req) => {
170
+ if (!ctx.hasUI) return undefined;
171
+ return (await ctx.ui.input(req.title, req.placeholder)) ?? undefined;
172
+ },
168
173
  title: `pi-ask — ${params.questions.length} question${params.questions.length > 1 ? "s" : ""}`,
169
174
  });
170
175
  },
@@ -184,14 +189,16 @@ export default function piAskExtension(pi: ExtensionAPI) {
184
189
  }
185
190
 
186
191
  const answers = result.answers ?? {};
192
+ const notes = result.notes ?? {};
187
193
  // Pretty-print for the LLM
188
194
  const out: string[] = ["User answers:"];
189
195
  for (let i = 0; i < params.questions.length; i++) {
190
196
  const q = params.questions[i];
191
197
  if (!q) continue;
192
198
  const a = answers[i];
199
+ let line: string;
193
200
  if (a === undefined) {
194
- out.push(` Q${i + 1} (${q.header}): (no answer)`);
201
+ line = ` Q${i + 1} (${q.header}): (no answer)`;
195
202
  } else if (Array.isArray(a)) {
196
203
  const parts: string[] = [];
197
204
  for (const item of a) {
@@ -201,17 +208,22 @@ export default function piAskExtension(pi: ExtensionAPI) {
201
208
  parts.push(`"${item}"`);
202
209
  }
203
210
  }
204
- out.push(` Q${i + 1} (${q.header}) [multi]: ${parts.join(", ")}`);
211
+ line = ` Q${i + 1} (${q.header}) [multi]: ${parts.join(", ")}`;
205
212
  } else if (typeof a === "number") {
206
- out.push(` Q${i + 1} (${q.header}): ${q.options[a]?.label ?? `?${a}`}`);
213
+ line = ` Q${i + 1} (${q.header}): ${q.options[a]?.label ?? `?${a}`}`;
207
214
  } else {
208
- out.push(` Q${i + 1} (${q.header}) [Other]: "${a}"`);
215
+ line = ` Q${i + 1} (${q.header}) [Other]: "${a}"`;
216
+ }
217
+ // Append note if present
218
+ if (notes[i]) {
219
+ line += ` // note: "${notes[i]}"`;
209
220
  }
221
+ out.push(line);
210
222
  }
211
223
 
212
224
  return {
213
225
  content: [{ type: "text", text: out.join("\n") }],
214
- details: { answers },
226
+ details: { answers, notes: Object.keys(notes).length > 0 ? notes : undefined },
215
227
  };
216
228
  },
217
229
  });
package/ask/picker.ts CHANGED
@@ -31,6 +31,13 @@ export interface AskOption {
31
31
  label: string;
32
32
  description?: string;
33
33
  recommended?: boolean;
34
+ /** Optional preview content shown in a side panel when this option is
35
+ * focused. Use markdown or plain text to show code snippets, structure
36
+ * examples, or elaboration of what the option entails.
37
+ *
38
+ * Example:
39
+ * preview: "```ts\nclass Auth {\n token: string\n}\n```" */
40
+ preview?: string;
34
41
  }
35
42
 
36
43
  export interface AskQuestion {
@@ -59,6 +66,10 @@ export interface AskProResult {
59
66
  cancelled?: boolean;
60
67
  /** Map of question index → answer. Single: number | string. Multi: (number | string)[] */
61
68
  answers?: Record<number, AskAnswer | AskMultiAnswer>;
69
+ /** Optional free-text notes the user added to specific questions.
70
+ * Keyed by question index. Added when the user pressed `n` after
71
+ * picking an option and typed a note. */
72
+ notes?: Record<number, string>;
62
73
  }
63
74
 
64
75
  /** Options for the text-input dialog opened when "Other…" is picked. */
@@ -80,6 +91,10 @@ interface AskProComponentDeps {
80
91
  * option is hidden even when `allowOther: true` (caller should ensure
81
92
  * the dependency is present if it advertises allowOther). */
82
93
  onRequestInput?: (req: AskProInputRequest) => Promise<string | undefined>;
94
+ /** Open a text-input dialog for adding a note to the current question
95
+ * (triggered by pressing `n`). Returns the typed text, or undefined
96
+ * if cancelled. If omitted, the `n` shortcut is a no-op. */
97
+ onRequestNote?: (req: AskProInputRequest) => Promise<string | undefined>;
83
98
  }
84
99
 
85
100
  // ---------------------------------------------------------------------------
@@ -107,12 +122,15 @@ export class AskProComponent extends Container {
107
122
  private keybindings: KeybindingsManager;
108
123
  private done: (result: AskProResult) => void;
109
124
  private onRequestInput?: (req: AskProInputRequest) => Promise<string | undefined>;
125
+ private onRequestNote?: (req: AskProInputRequest) => Promise<string | undefined>;
110
126
  private title: string;
111
127
 
112
128
  private currentIndex = 0;
113
129
  private selectedIndex = 0;
114
130
  /** answers[questionIdx] = AskAnswer (single) or AskMultiAnswer (multi). */
115
131
  private answers = new Map<number, AskAnswer | AskMultiAnswer>();
132
+ /** notes[questionIdx] = free-text note added by user (via `n` key). */
133
+ private notes = new Map<number, string>();
116
134
  /** Set true once `done` is called — further input is ignored. */
117
135
  private completed = false;
118
136
  /** Set while a text-input dialog is awaiting the user's reply. */
@@ -120,6 +138,7 @@ export class AskProComponent extends Container {
120
138
 
121
139
  private tabsText!: Text;
122
140
  private bodyContainer!: Container;
141
+ private previewText!: Text;
123
142
  private footerText!: Text;
124
143
 
125
144
  constructor(deps: AskProComponentDeps) {
@@ -129,6 +148,7 @@ export class AskProComponent extends Container {
129
148
  this.keybindings = deps.keybindings;
130
149
  this.done = deps.done;
131
150
  this.onRequestInput = deps.onRequestInput;
151
+ this.onRequestNote = deps.onRequestNote;
132
152
  this.title = deps.title ?? "pi-ask";
133
153
 
134
154
  const titleText = new Text(this.theme.fg("accent", this.theme.bold(this.title)), 1, 0);
@@ -194,6 +214,11 @@ export class AskProComponent extends Container {
194
214
  const q = this.questions[this.currentIndex];
195
215
  if (!q) return;
196
216
 
217
+ // Compute the current preview (from the option under the cursor).
218
+ // Shown side-by-side with the option list via pad-right.
219
+ const currentPreview = this.currentPreviewLines();
220
+ const hasPreview = currentPreview.length > 0;
221
+
197
222
  // Question line: "Q1 of 3: <question>"
198
223
  this.bodyContainer.addChild(
199
224
  new Text(
@@ -397,6 +422,12 @@ export class AskProComponent extends Container {
397
422
  // Single-select: Enter is the action key
398
423
  parts.push(this.theme.fg("accent", isLast ? "⏎ submit" : "⏎ next"));
399
424
  }
425
+ // `n` hint: add/edit note (only if dep is wired)
426
+ if (this.onRequestNote) {
427
+ const hasNote = this.notes.has(this.currentIndex);
428
+ const hint = hasNote ? "n ✓note" : "n note";
429
+ parts.push(this.theme.fg(hasNote ? "success" : "dim", hint));
430
+ }
400
431
  parts.push(this.theme.fg("dim", "esc cancel"));
401
432
  return parts.join(" ");
402
433
  }
@@ -565,6 +596,13 @@ export class AskProComponent extends Container {
565
596
  }
566
597
  return;
567
598
  }
599
+
600
+ // `n` — add/edit a free-text note for the current question.
601
+ // Requires onRequestNote dep; otherwise ignored.
602
+ if (keyData === "n" && this.onRequestNote) {
603
+ void this.requestNoteInput();
604
+ return;
605
+ }
568
606
  }
569
607
 
570
608
  private handlePick(optionIdx: number): void {
@@ -659,14 +697,54 @@ export class AskProComponent extends Container {
659
697
  }
660
698
  }
661
699
 
700
+ /** Open a text-input dialog to add/edit a note for the current question.
701
+ * Triggered by the `n` key. Pre-fills with any existing note so the
702
+ * user can edit it. An empty submission clears the note. */
703
+ private async requestNoteInput(): Promise<void> {
704
+ if (!this.onRequestNote) return;
705
+ const q = this.questions[this.currentIndex];
706
+ if (!q) return;
707
+ this.awaitingInput = true;
708
+ const existing = this.notes.get(this.currentIndex) ?? "";
709
+ const text = await this.onRequestNote({
710
+ title: q.header,
711
+ prompt: `Add a note to your answer for: ${q.question}`,
712
+ placeholder: existing || "Add context, edge cases, or reasoning…",
713
+ });
714
+ this.awaitingInput = false;
715
+ if (text === undefined) {
716
+ // Cancelled — keep existing note, just redraw
717
+ this.repaint();
718
+ return;
719
+ }
720
+ const trimmed = text.trim();
721
+ if (trimmed === "") {
722
+ // Empty submission clears the note
723
+ this.notes.delete(this.currentIndex);
724
+ } else {
725
+ this.notes.set(this.currentIndex, trimmed);
726
+ }
727
+ this.repaint();
728
+ }
729
+
662
730
  private submit(): void {
663
731
  if (!this.allAnswered()) return;
664
732
  const answers: Record<number, AskAnswer | AskMultiAnswer> = {};
665
733
  for (let i = 0; i < this.questions.length; i++) {
666
734
  answers[i] = this.answers.get(i) as AskAnswer | AskMultiAnswer;
667
735
  }
736
+ // Include notes only if at least one was added
737
+ const notes: Record<number, string> = {};
738
+ let hasNotes = false;
739
+ for (let i = 0; i < this.questions.length; i++) {
740
+ const n = this.notes.get(i);
741
+ if (n) {
742
+ notes[i] = n;
743
+ hasNotes = true;
744
+ }
745
+ }
668
746
  this.completed = true;
669
- this.done({ answers });
747
+ this.done(hasNotes ? { answers, notes } : { answers });
670
748
  }
671
749
 
672
750
  // -------------------------------------------------------------------------
@@ -678,6 +756,103 @@ export class AskProComponent extends Container {
678
756
  this.completed = true;
679
757
  this.awaitingInput = false;
680
758
  }
759
+
760
+ // -----------------------------------------------------------------------
761
+ // Side-by-side render: stack picker column (left) with preview column
762
+ // (right) row-by-row. Each visible body row gets padded to SPLIT_COL and
763
+ // the matching preview row is appended. If no preview is set, the left
764
+ // column takes the full width (no padding).
765
+ // -----------------------------------------------------------------------
766
+ private static readonly SPLIT_COL = 60; // picker column width when preview is present
767
+ private static readonly PREVIEW_COL = 60; // preview column width
768
+
769
+ /** Lines of preview content for the option currently under the cursor.
770
+ * Returns [] when no option is focused or the option has no preview. */
771
+ private currentPreviewLines(): string[] {
772
+ const q = this.questions[this.currentIndex];
773
+ if (!q) return [];
774
+ // Only real options carry previews (not "Other…")
775
+ if (this.selectedIndex < 0 || this.selectedIndex >= q.options.length) return [];
776
+ const opt = q.options[this.selectedIndex];
777
+ if (!opt?.preview) return [];
778
+ // Trim and split on newlines; drop leading/trailing blank lines.
779
+ const lines = opt.preview
780
+ .replace(/\r\n/g, "\n")
781
+ .split("\n")
782
+ .map((l) => l.trimEnd());
783
+ while (lines.length > 0 && lines[0]?.trim() === "") lines.shift();
784
+ while (lines.length > 0 && lines[lines.length - 1]?.trim() === "") lines.pop();
785
+ return lines;
786
+ }
787
+
788
+ /** Override render to produce a side-by-side layout when a preview is
789
+ * present. Falls back to default Container render otherwise. */
790
+ render(width: number): string[] {
791
+ const superLines = super.render(width);
792
+ const previewLines = this.currentPreviewLines();
793
+ if (previewLines.length === 0) return superLines;
794
+
795
+ // We need to find which body lines belong to the option list and
796
+ // merge the preview next to them. Simpler approach: append a preview
797
+ // block below the picker body, styled as a framed right-aligned panel.
798
+ // True side-by-side would require row index tracking which Container
799
+ // doesn't expose; the framed block is visually distinct and avoids
800
+ // fragile index math.
801
+ const splitCol = Math.min(AskProComponent.SPLIT_COL, Math.floor(width * 0.6));
802
+ const previewWidth = Math.max(30, width - splitCol - 3);
803
+ const border = this.theme.fg("dim", "│");
804
+
805
+ // Wrap preview lines to previewWidth
806
+ const wrapped: string[] = [];
807
+ for (const line of previewLines) {
808
+ if (line.length === 0) {
809
+ wrapped.push("");
810
+ continue;
811
+ }
812
+ // Simple greedy word wrap
813
+ const words = line.split(" ");
814
+ let cur = "";
815
+ for (const w of words) {
816
+ if (cur.length === 0) {
817
+ cur = w;
818
+ } else if (cur.length + 1 + w.length <= previewWidth) {
819
+ cur += " " + w;
820
+ } else {
821
+ wrapped.push(cur);
822
+ cur = w;
823
+ }
824
+ }
825
+ if (cur) wrapped.push(cur);
826
+ }
827
+
828
+ // Insert preview as a side panel: pad each picker line to splitCol,
829
+ // then add a vertical border and the matching preview line.
830
+ // We do this for ALL super lines so the preview spans the picker's
831
+ // full height.
832
+ const result: string[] = [];
833
+ // Header row above the preview content
834
+ result.push("".padEnd(splitCol) + " " + border + " " + this.theme.fg("dim", "— preview —"));
835
+ for (let i = 0; i < superLines.length; i++) {
836
+ const superLine = superLines[i] ?? "";
837
+ // Strip ANSI for width measurement
838
+ const visibleLen = superLine.replace(/\x1b\[[0-9;]*m/g, "").length;
839
+ const pad = Math.max(1, splitCol - visibleLen);
840
+ let row = superLine + " ".repeat(pad) + border + " ";
841
+ const pLine = wrapped[i];
842
+ if (pLine !== undefined) {
843
+ row += this.theme.fg("text", pLine);
844
+ }
845
+ result.push(row);
846
+ }
847
+ // If preview has more lines than the picker, append them below
848
+ if (wrapped.length > superLines.length) {
849
+ for (let i = superLines.length; i < wrapped.length; i++) {
850
+ const pLine = wrapped[i] ?? "";
851
+ result.push(" ".repeat(splitCol + 1) + border + " " + this.theme.fg("text", pLine));
852
+ }
853
+ }
854
+ return result;
855
+ }
681
856
  }
682
857
 
683
858
  /** Type guard for the public component. */
package/ask/prompt.ts CHANGED
@@ -30,8 +30,12 @@ DON'T use it for:
30
30
  - When the user already gave a clear answer — don't second-guess
31
31
  - Trivial clarifications — use plain text first, escalate to \`ask_pro\` only if the answer matters
32
32
 
33
- Keyboard in the picker: \`↑↓\` navigate, \`1-N\` instant-pick, \`Tab\` next question, \`Space\` toggle (multi-select only), \`Enter\` confirm/advance/submit, \`Esc\` cancel.
33
+ Keyboard in the picker: \`↑↓\` navigate, \`1-N\` instant-pick, \`Tab\` next question, \`Space\` toggle (multi-select only), \`Enter\` confirm/advance/submit, \`n\` add a free-text note to the current question, \`Esc\` cancel.
34
34
 
35
- Schema reminder: \`questions: [{ header, question, options: [{label, description?, recommended?}], multiSelect? }]\`. Mark exactly one option \`recommended: true\` per question when you have a default.
35
+ Schema reminder: \`questions: [{ header, question, options: [{label, description?, recommended?, preview?}], multiSelect? }]\`. Mark exactly one option \`recommended: true\` per question when you have a default.
36
+
37
+ **Option previews:** \`option.preview\` (markdown/plain string) shows in a side panel next to the option list while that option is focused. Use it when the question is about a code structure, API shape, or concrete example — show a small snippet of what each option entails so the user can decide without asking follow-ups. Example: when asking "how should we model auth?", each option's preview can show the relevant type signature.
38
+
39
+ **Notes:** the user can press \`n\` after picking an answer to attach a free-text note (edge cases, constraints, reasoning). The note is returned to you as \`// note: \"...\"\` next to the chosen answer. Treat it as a hard constraint.
36
40
  `;
37
41
  }
@@ -586,3 +586,165 @@ describe("AskProComponent — Other… option (allowOther)", () => {
586
586
  expect(picker.getSelectedIndex()).toBeLessThan(2);
587
587
  });
588
588
  });
589
+
590
+ // ---------------------------------------------------------------------------
591
+ // Option previews (side-by-side)
592
+ // ---------------------------------------------------------------------------
593
+
594
+ describe("AskProComponent — option previews", () => {
595
+ test("currentPreviewLines returns [] when option has no preview", () => {
596
+ const { picker } = setup();
597
+ // Type-level access: currentPreviewLines is private; verify via render
598
+ // (no preview lines means render falls back to super.render width-only)
599
+ const lines = picker.render(80);
600
+ // Just verify it doesn't throw and returns some lines
601
+ expect(lines.length).toBeGreaterThan(0);
602
+ });
603
+
604
+ test("render includes preview content when option has preview", () => {
605
+ const qsWithPreview: AskQuestion[] = [
606
+ {
607
+ header: "Model",
608
+ question: "Which schema?",
609
+ options: [
610
+ {
611
+ label: "Relational",
612
+ description: "Postgres + strict schema",
613
+ preview: "CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n email TEXT NOT NULL\n);",
614
+ },
615
+ { label: "Document", description: "MongoDB" },
616
+ ],
617
+ },
618
+ ];
619
+ const { picker } = setup(qsWithPreview);
620
+ const lines = picker.render(100);
621
+ const joined = lines.join("\n");
622
+ // Preview content should appear in the rendered output
623
+ expect(joined).toContain("CREATE TABLE");
624
+ expect(joined).toContain("preview");
625
+ });
626
+
627
+ test("preview disappears when focused option has none", () => {
628
+ const qsWithPreview: AskQuestion[] = [
629
+ {
630
+ header: "Model",
631
+ question: "Which schema?",
632
+ options: [
633
+ {
634
+ label: "Relational",
635
+ description: "Postgres",
636
+ preview: "CREATE TABLE users (id INT);",
637
+ },
638
+ { label: "Document", description: "MongoDB" },
639
+ ],
640
+ },
641
+ ];
642
+ const { picker } = setup(qsWithPreview);
643
+ // Focus option 0 (has preview)
644
+ picker.handleInput("j"); // wait, default 0 already; move to 1
645
+ picker.handleInput("j"); // option 1 (no preview)
646
+ const lines = picker.render(100);
647
+ const joined = lines.join("\n");
648
+ // No preview lines for option without preview
649
+ expect(joined).not.toContain("CREATE TABLE");
650
+ });
651
+ });
652
+
653
+ // ---------------------------------------------------------------------------
654
+ // Notes (`n` key)
655
+ // ---------------------------------------------------------------------------
656
+
657
+ describe("AskProComponent — notes (n key)", () => {
658
+ test("n is a no-op when onRequestNote is not provided", () => {
659
+ const { picker } = setup();
660
+ // Should not throw, no effect
661
+ expect(() => picker.handleInput("n")).not.toThrow();
662
+ });
663
+
664
+ test("n opens note dialog when onRequestNote is provided", async () => {
665
+ let noteRequestCount = 0;
666
+ let resolveNote: (value: string | undefined) => void = () => {};
667
+ const picker = new AskProComponent({
668
+ questions: sampleQuestions,
669
+ theme,
670
+ keybindings,
671
+ done: () => {},
672
+ onRequestNote: async () => {
673
+ noteRequestCount++;
674
+ return new Promise<string | undefined>((r) => {
675
+ resolveNote = r;
676
+ });
677
+ },
678
+ });
679
+ // `n` should trigger note request
680
+ expect(noteRequestCount).toBe(0);
681
+ picker.handleInput("n");
682
+ // Microtask to let async start
683
+ await Promise.resolve();
684
+ expect(noteRequestCount).toBe(1);
685
+ // Resolve with a note
686
+ resolveNote("only for prod, use TLS");
687
+ await Promise.resolve();
688
+ });
689
+
690
+ test("note is included in submit result", async () => {
691
+ let doneResult: AskProResult | null = null;
692
+ const pendingNotes: Array<(v: string | undefined) => void> = [];
693
+ const picker = new AskProComponent({
694
+ questions: sampleQuestions,
695
+ theme,
696
+ keybindings,
697
+ done: (r) => {
698
+ doneResult = r;
699
+ },
700
+ onRequestNote: async () => {
701
+ return new Promise<string | undefined>((resolve) => {
702
+ pendingNotes.push(resolve);
703
+ });
704
+ },
705
+ });
706
+ // Pick Q1 → advance to Q2
707
+ picker.handleInput("1");
708
+ // Add note to Q2
709
+ picker.handleInput("n");
710
+ // Wait for the promise to register
711
+ await Promise.resolve();
712
+ await Promise.resolve();
713
+ expect(pendingNotes.length).toBe(1);
714
+ pendingNotes[0]!("rotate keys monthly");
715
+ // Wait for the dialog to settle
716
+ await Promise.resolve();
717
+ await Promise.resolve();
718
+ // Submit Q2 (last question, all answered)
719
+ picker.handleInput("1");
720
+ expect(doneResult).not.toBeNull();
721
+ const result = doneResult as AskProResult | null;
722
+ expect(result?.notes).toBeDefined();
723
+ expect(result?.notes?.[1]).toBe("rotate keys monthly");
724
+ });
725
+
726
+ test("empty note submission clears existing note", async () => {
727
+ let resolveNote: (value: string | undefined) => void = () => {};
728
+ const picker = new AskProComponent({
729
+ questions: sampleQuestions,
730
+ theme,
731
+ keybindings,
732
+ done: () => {},
733
+ onRequestNote: async () => {
734
+ return new Promise<string | undefined>((r) => {
735
+ resolveNote = r;
736
+ });
737
+ },
738
+ });
739
+ // Add a note
740
+ picker.handleInput("n");
741
+ resolveNote("first note");
742
+ await Promise.resolve();
743
+ // Clear it with empty
744
+ picker.handleInput("n");
745
+ resolveNote(" ");
746
+ await Promise.resolve();
747
+ // No crash — note cleared
748
+ expect(() => picker.render(80)).not.toThrow();
749
+ });
750
+ });
package/commands.ts CHANGED
@@ -51,6 +51,7 @@ export interface CommandUI {
51
51
  notify: (text: string, kind?: "info" | "warning" | "error") => void;
52
52
  select: (label: string, options: string[]) => Promise<number | null>;
53
53
  confirm: (title: string, message: string) => Promise<boolean>;
54
+ input?: (label: string, placeholder?: string) => Promise<string | undefined>;
54
55
  }
55
56
 
56
57
  export interface CommandsDeps {
@@ -430,7 +431,13 @@ What must the LLM do?
430
431
  | "minimal" | "web-app" | "library" | "cli" | undefined) ?? undefined;
431
432
  const autoYes = args.includes("--yes");
432
433
  const projectName = args.match(/--name[= ](\S+)/)?.[1];
433
- await initSolyProject(ctx.cwd, ui, { template, autoYes, projectName });
434
+ const initUi = {
435
+ notify: (t: string, k?: "info" | "warning" | "error") => ctx.ui.notify(t, k ?? "info"),
436
+ select: async (label: string, options: string[]) => ctx.ui.select(label, options),
437
+ confirm: (title: string, message: string) => ctx.ui.confirm(title, message),
438
+ input: (label: string, placeholder?: string) => ctx.ui.input(label, placeholder),
439
+ };
440
+ await initSolyProject(ctx.cwd, initUi, { template, autoYes, projectName });
434
441
  },
435
442
  });
436
443
 
package/core.ts CHANGED
@@ -26,7 +26,10 @@ import * as path from "node:path";
26
26
  export type RuleSource =
27
27
  | "project-soly"
28
28
  | "global-soly"
29
- | "phase-soly";
29
+ | "phase-soly"
30
+ | "project-agents"
31
+ | "global-agents"
32
+ | "phase-agents";
30
33
 
31
34
  export interface RuleFrontmatter {
32
35
  description?: string;
@@ -59,7 +62,7 @@ export interface RuleFile {
59
62
  enabled: boolean;
60
63
  mtimeMs: number;
61
64
  source: RuleSource;
62
- sourceLabel: "soly" | "phase" | "local";
65
+ sourceLabel: "soly" | "phase" | "local" | "agents";
63
66
  priority: number; // higher wins on relPath collision
64
67
  /** Phase number for phase-scoped rules; undefined otherwise. */
65
68
  phaseNumber?: number;
@@ -70,7 +73,7 @@ export interface RuleFile {
70
73
  export interface SourceSpec {
71
74
  dir: string;
72
75
  source: RuleSource;
73
- sourceLabel: "soly" | "phase" | "local";
76
+ sourceLabel: "soly" | "phase" | "local" | "agents";
74
77
  priority: number; // higher wins on relPath collision
75
78
  /** Optional phase number (for phase-scoped sources). */
76
79
  phaseNumber?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-soly",
3
- "version": "1.9.2",
3
+ "version": "1.10.0",
4
4
  "description": "Project management + workflow engine for pi-coding-agent. Plans, state, MANDATORY rules, multi-question picker — one npm install, zero config. LLM is the executor (no subagent layer).",
5
5
  "type": "module",
6
6
  "main": "index.ts",