pi-soly 1.13.5 → 1.14.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/README.md CHANGED
@@ -114,8 +114,7 @@ soly status # current position + progress (no LLM round-trip)
114
114
  ### Setup
115
115
 
116
116
  ```bash
117
- /soly-init # scaffold .soly/ (templates: minimal|web-app|library|cli)
118
- /soly-migrate # atomic .soly/ → .agents/ rename (vendor-neutral)
117
+ /soly init # scaffold .agents/ (templates: minimal|web-app|library|cli)
119
118
  /soly-status # one-screen health report
120
119
  /soly-log # recent notifications
121
120
  ```
@@ -133,7 +132,7 @@ soly status # current position + progress (no LLM round-trip)
133
132
 
134
133
  Two system-prompt injections, both **opt-in** and **fully observable**.
135
134
 
136
- ### Rules — `.soly/rules/` or `~/.soly/rules/`
135
+ ### Rules — `.agents/rules/` or `~/.agents/rules/`
137
136
 
138
137
  Markdown files with frontmatter. Three modes:
139
138
 
@@ -167,7 +166,7 @@ modify. If a rule contradicts your instinct, the rule wins.**
167
166
 
168
167
  See context breakdown anytime: `/rules stats`.
169
168
 
170
- ### Docs — `.soly/docs/` or `~/.soly/docs/`
169
+ ### Docs — `.agents/docs/` or `~/.agents/docs/`
171
170
 
172
171
  Zero-point intent docs (your vision, business context). Loaded as **preview only** (180 chars per doc) — cheap. Add `inline: true` to opt-in to full body injection.
173
172
 
@@ -229,7 +228,7 @@ Flip cards with ←/→ (or 1-N), choose with Enter, Esc to cancel. Native TUI
229
228
 
230
229
  ## 🖼 HTML Artifacts
231
230
 
232
- `html_artifact` tool for the LLM — soly's local "artifacts". Renders HTML (a full document or just body content, themed light/dark) and serves it from a **per-session gallery SPA** — a sidebar of every artifact this session, an iframe viewer, a filter box, a light/dark toggle, and live SSE updates — on one stable localhost URL, opened in your browser. Pass `id` to update an artifact in place; pass `assets` to write sibling files (images/css/json) the HTML references; restyle everything via `.soly/artifact-theme.css`. (Falls back to opening the file directly if the server is disabled.)
231
+ `html_artifact` tool for the LLM — soly's local "artifacts". Renders HTML (a full document or just body content, themed light/dark) and serves it from a **per-session gallery SPA** — a sidebar of every artifact this session, an iframe viewer, a filter box, a light/dark toggle, and live SSE updates — on one stable localhost URL, opened in your browser. Pass `id` to update an artifact in place; pass `assets` to write sibling files (images/css/json) the HTML references; restyle everything via `.agents/artifact-theme.css`. (Falls back to opening the file directly if the server is disabled.)
233
232
 
234
233
  ```ts
235
234
  html_artifact({
@@ -272,7 +271,7 @@ Use it when a visual, rendered result beats terminal text (example galleries, co
272
271
 
273
272
  ┌────────────┼────────────┐
274
273
  ▼ ▼ ▼
275
- .soly/STATE phases/<N>/ rules/
274
+ .agents/STATE phases/<N>/ rules/
276
275
  (current CONTEXT, docs/
277
276
  position) PLAN, (system
278
277
  RESEARCH) prompt)
@@ -288,10 +287,10 @@ Use it when a visual, rendered result beats terminal text (example galleries, co
288
287
  └──────────────────┘
289
288
  ```
290
289
 
291
- State lives in `.soly/` — portable, git-friendly, human-readable. Migrate to vendor-neutral `.agents/` with `/soly-migrate` (both supported, `.soly/` shows deprecation warning).
290
+ State lives in `.agents/` — portable, git-friendly, human-readable. (Projects from before the rename used `.soly/`; soly no longer reads it — run `mv .soly .agents`.)
292
291
 
293
292
  ```
294
- .soly/
293
+ .agents/
295
294
  ├── ROADMAP.md # phase table
296
295
  ├── STATE.md # current position + decisions log
297
296
  ├── docs/ # 0-point intent docs (preview-loaded)
@@ -306,7 +305,7 @@ State lives in `.soly/` — portable, git-friendly, human-readable. Migrate to v
306
305
  └── SUMMARY.md
307
306
  ```
308
307
 
309
- > Legacy projects (standalone `NN-MM-PLAN.md` / a `features/` dir) still work; run `soly migrate` to convert them to this layout.
308
+ > Legacy projects (standalone `NN-MM-PLAN.md` files / a `features/` dir) still load and run alongside the unified `phases/<N>/tasks/` layout.
310
309
 
311
310
  ---
312
311
 
package/artifact/index.ts CHANGED
@@ -31,13 +31,13 @@ import {
31
31
  type ToolText = { content: { type: "text"; text: string }[]; details: Record<string, unknown> };
32
32
  type Asset = { path: string; content: string; encoding?: string };
33
33
 
34
- /** Load the artifact CSS theme: config override → .soly/artifact-theme.css →
34
+ /** Load the artifact CSS theme: config override → .agents/artifact-theme.css →
35
35
  * built-in DEFAULT_CSS. */
36
36
  function loadCss(themeCfg: string, cwd: string): string {
37
37
  const candidates: string[] = [];
38
38
  const t = themeCfg.trim();
39
39
  if (t) candidates.push(path.isAbsolute(t) ? t : path.join(cwd, t));
40
- candidates.push(path.join(cwd, ".soly", "artifact-theme.css"));
40
+ candidates.push(path.join(cwd, ".agents", "artifact-theme.css"));
41
41
  for (const c of candidates) {
42
42
  try {
43
43
  return fs.readFileSync(c, "utf-8");
@@ -150,7 +150,7 @@ export default function piArtifactExtension(pi: ExtensionAPI, getConfig: () => S
150
150
  name: "html_artifact",
151
151
  label: "soly · html_artifact",
152
152
  description:
153
- "Render HTML to a self-contained file and serve it from a per-project gallery in the browser — soly's artifacts. `html` is a full document or a body fragment (wrapped in a styled light/dark skeleton; theme overridable via .soly/artifact-theme.css). Pass `id` to update an existing artifact in place (re-render). Pass `assets` to write sibling files (images/css/json) the HTML references via relative paths. Use when a visual rendered result beats terminal text. Self-contained otherwise — no external URLs. The gallery is shared across every pi window in this folder and survives restarts. Returns the localhost URL + gallery.",
153
+ "Render HTML to a self-contained file and serve it from a per-project gallery in the browser — soly's artifacts. `html` is a full document or a body fragment (wrapped in a styled light/dark skeleton; theme overridable via .agents/artifact-theme.css). Pass `id` to update an existing artifact in place (re-render). Pass `assets` to write sibling files (images/css/json) the HTML references via relative paths. Use when a visual rendered result beats terminal text. Self-contained otherwise — no external URLs. The gallery is shared across every pi window in this folder and survives restarts. Returns the localhost URL + gallery.",
154
154
  parameters: Type.Object({
155
155
  title: Type.String({ description: "Title (used for <title>, header, gallery, filename)." }),
156
156
  html: Type.String({
@@ -6,7 +6,7 @@
6
6
  // session gallery shell — soly's local "artifacts". Pure: every function takes
7
7
  // strings and returns strings; the stylesheet is passed in (loaded from a file
8
8
  // or the built-in DEFAULT_CSS by the caller) so a project can restyle every
9
- // artifact by dropping a `.soly/artifact-theme.css`.
9
+ // artifact by dropping a `.agents/artifact-theme.css`.
10
10
  //
11
11
  // A body *fragment* is wrapped in the skeleton; a *full document* keeps its own
12
12
  // markup but gets the theme injected as a base layer (its own styles win).
@@ -155,7 +155,7 @@ export function buildGalleryShell(token: string): string {
155
155
  }
156
156
 
157
157
  // Self-contained artifact stylesheet — no external fonts, scripts, or CDN
158
- // requests. Overridable per project via .soly/artifact-theme.css.
158
+ // requests. Overridable per project via .agents/artifact-theme.css.
159
159
  export const DEFAULT_CSS = `
160
160
  :root{color-scheme:light dark}
161
161
  *{box-sizing:border-box}
package/ask/README.md CHANGED
@@ -18,7 +18,9 @@ showing a **tabbed, multi-question picker** in pi's TUI.
18
18
  typed-answer question; it's optional (blank is allowed)
19
19
  - **Option previews** — per-option `preview` shows a side panel while focused;
20
20
  fenced ```code blocks in it are syntax-highlighted
21
- - **"Other…"** — per-question `allowOther: true` adds a free-text custom choice
21
+ - **"Other…"** — every options question automatically gets a free-text custom
22
+ choice (the `ask_pro` tool forces `allowOther: true`), so the user can always
23
+ answer in their own words; `freeText` questions are already free-form
22
24
  - **Notes & skip** — `n` attaches a free-text note to any answer; `s` skips a
23
25
  question (returned in `skipped`)
24
26
  - **Cancelled detection** — `Esc` resolves `{cancelled: true}`
package/ask/index.ts CHANGED
@@ -71,7 +71,7 @@ export default function piAskExtension(pi: ExtensionAPI) {
71
71
  name: "ask_pro",
72
72
  label: "soly · ask_pro",
73
73
  description:
74
- "Ask the user one or more questions at once via a tabbed picker (≤6 questions); returns all answers in one call. USE WHEN: (a) you have 2+ related questions to gather in one batch, OR (b) the choice is local — a sub-question inside an already-decided theme, or a simple label-vs-label «или/или» that doesn't justify a full-screen deck. Each option is a short label + 1-2 sentence description (no per-option code, no pros/cons). For a global architectural fork with real counterweight between options, use `decision_deck` instead. Per question: single-select (default), `multiSelect` (+ `minSelect`/`maxSelect`), `allowOther` (free-text 'Other…'), or `freeText` (typed answer, no options). Per option: `recommended` (⭐) and `preview` (side panel, fenced code highlighted). User can skip (`s`) or note (`n`). Prefer this over one-by-one questions.",
74
+ "Ask the user one or more questions at once via a tabbed picker (≤6 questions); returns all answers in one call. USE WHEN: (a) you have 2+ related questions to gather in one batch, OR (b) the choice is local — a sub-question inside an already-decided theme, or a simple label-vs-label «или/или» that doesn't justify a full-screen deck. Each option is a short label + 1-2 sentence description (no per-option code, no pros/cons). For a global architectural fork with real counterweight between options, use `decision_deck` instead. Per question: single-select (default), `multiSelect` (+ `minSelect`/`maxSelect`), or `freeText` (typed answer, no options). **Every options question automatically includes a free-text 'Other…' choice the user can always answer in their own words, so never add a manual «other/custom» option yourself.** Per option: `recommended` (⭐) and `preview` (side panel, fenced code highlighted). User can skip (`s`) or note (`n`). Prefer this over one-by-one questions.",
75
75
  parameters: Type.Object({
76
76
  questions: Type.Array(
77
77
  Type.Object({
@@ -99,7 +99,10 @@ export default function piAskExtension(pi: ExtensionAPI) {
99
99
  Type.Boolean({ description: "Allow multiple picks (default single-select)." }),
100
100
  ),
101
101
  allowOther: Type.Optional(
102
- Type.Boolean({ description: "Add a free-text 'Other…' choice." }),
102
+ Type.Boolean({
103
+ description:
104
+ "Ignored — a free-text 'Other…' choice is always added to every options question automatically. Kept for backward compatibility.",
105
+ }),
103
106
  ),
104
107
  minSelect: Type.Optional(
105
108
  Type.Number({ description: "Multi-select: min picks (default 1)." }),
@@ -150,6 +153,17 @@ export default function piAskExtension(pi: ExtensionAPI) {
150
153
  details: { error: "too_many_questions", count: params.questions.length },
151
154
  };
152
155
  }
156
+
157
+ // Always offer a free-text "Other…" on every options question, so the
158
+ // user can answer in their own words even when no option fits. This is
159
+ // enforced here (not left to the model) so the escape hatch is
160
+ // guaranteed on every question. `freeText` questions are already
161
+ // free-form, so leave them untouched. Done before validation so the
162
+ // synthetic "Other…" slot is counted in multi-select bounds.
163
+ for (const q of params.questions) {
164
+ if (q && !q.freeText) q.allowOther = true;
165
+ }
166
+
153
167
  for (let i = 0; i < params.questions.length; i++) {
154
168
  const q = params.questions[i];
155
169
  if (!q) continue;
package/codemap.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  // live without having to `ls` its way around.
9
9
  //
10
10
  // Heuristics:
11
- // - skip noise: node_modules, .git, dist, build, .soly, .next, out, target,
11
+ // - skip noise: node_modules, .git, dist, build, .agents, .next, out, target,
12
12
  // coverage, .venv, __pycache__
13
13
  // - depth: walk 2 levels deep by default (cwd → dirs → subdirs)
14
14
  // - key files: package.json, tsconfig.json, README.md, Cargo.toml, go.mod,
@@ -23,7 +23,7 @@ import * as path from "node:path";
23
23
  const IGNORE_DIRS = new Set([
24
24
  "node_modules",
25
25
  ".git",
26
- ".soly",
26
+ ".agents",
27
27
  "dist",
28
28
  "build",
29
29
  "out",
package/commands.ts CHANGED
@@ -43,7 +43,6 @@ import {
43
43
  type IntentInlineDoc,
44
44
  } from "./intent.ts";
45
45
  import type { SolyConfig } from "./config.ts";
46
- import { migrateSolyDir } from "./migrate.js";
47
46
  import { initSolyProject } from "./init.js";
48
47
  import { ListPanel, type ListItem, type ListAction } from "./visual/list-panel.ts";
49
48
  import { getArtifactServer, ensureArtifactServer, artifactDir } from "./artifact/session.ts";
@@ -367,7 +366,7 @@ What must the LLM do?
367
366
  return;
368
367
  }
369
368
 
370
- // /rules add <url> — download a remote rule into .soly/rules/
369
+ // /rules add <url> — download a remote rule into .agents/rules/
371
370
  if (sub === "add") {
372
371
  const url = (target ?? "").trim();
373
372
  if (!url) {
@@ -461,7 +460,7 @@ What must the LLM do?
461
460
  if (sub === "list") {
462
461
  const docs = getIntentDocs();
463
462
  if (docs.length === 0) {
464
- ui.notify("no intent docs found in .soly/docs/ — drop your vision/domain docs there", "info");
463
+ ui.notify("no intent docs found in .agents/docs/ — drop your vision/domain docs there", "info");
465
464
  return;
466
465
  }
467
466
  if (ctx.mode === "tui") {
@@ -500,56 +499,7 @@ What must the LLM do?
500
499
  },
501
500
  });
502
501
 
503
- // ============================================================================
504
- // /soly migrate — move .soly/ → .agents/ atomically
505
- // ============================================================================
506
- pi.registerCommand("soly-migrate", {
507
- description:
508
- "migrate project state from .soly/ to .agents/ (atomic rename, validates result, suggests git commit)",
509
- handler: async (args, ctx) => {
510
- const ui: CommandUI = {
511
- notify: (t, k) => ctx.ui.notify(t, k ?? "info"),
512
- select: async (label, options) => {
513
- const result = await ctx.ui.select(label, options);
514
- return result === undefined ? null : options.indexOf(result);
515
- },
516
- confirm: (title, message) => ctx.ui.confirm(title, message),
517
- };
518
- await migrateSolyDir(ctx.cwd, ui, { autoYes: args.includes("--yes") });
519
- },
520
- });
521
-
522
- // /soly init — scaffold new project
523
- // ============================================================================
524
- pi.registerCommand("soly-init", {
525
- description:
526
- "scaffold a new soly project (interactive: pick template — minimal/web-app/library/cli)",
527
- handler: async (args, ctx) => {
528
- const ui: CommandUI = {
529
- notify: (t, k) => ctx.ui.notify(t, k ?? "info"),
530
- select: async (label, options) => {
531
- const result = await ctx.ui.select(label, options);
532
- return result === undefined ? null : options.indexOf(result);
533
- },
534
- confirm: (title, message) => ctx.ui.confirm(title, message),
535
- input: (label, placeholder) => ctx.ui.input(label, placeholder),
536
- };
537
- // Parse args: --template=X, --yes, --name=X
538
- const template = (args.match(/--template[= ](\S+)/)?.[1] as
539
- | "minimal" | "web-app" | "library" | "cli" | undefined) ?? undefined;
540
- const autoYes = args.includes("--yes");
541
- const projectName = args.match(/--name[= ](\S+)/)?.[1];
542
- const initUi = {
543
- notify: (t: string, k?: "info" | "warning" | "error") => ctx.ui.notify(t, k ?? "info"),
544
- select: async (label: string, options: string[]) => ctx.ui.select(label, options),
545
- confirm: (title: string, message: string) => ctx.ui.confirm(title, message),
546
- input: (label: string, placeholder?: string) => ctx.ui.input(label, placeholder),
547
- };
548
- await initSolyProject(ctx.cwd, initUi, { template, autoYes, projectName });
549
- },
550
- });
551
-
552
- // /soly
502
+ // /soly (also hosts `/soly init` — see the early dispatch in the handler)
553
503
  // ============================================================================
554
504
 
555
505
  pi.registerCommand("soly", {
@@ -564,9 +514,30 @@ What must the LLM do?
564
514
  },
565
515
  confirm: (title, message) => ctx.ui.confirm(title, message),
566
516
  };
517
+ // `init` is special — it scaffolds a NEW project, so it must run even
518
+ // when there's no `.agents/` dir yet. Dispatch it before the project
519
+ // guard below. (Moved here from the old standalone `/soly-init`.)
520
+ {
521
+ const firstArg = args.trim().split(/\s+/).filter(Boolean)[0] ?? "";
522
+ if (firstArg === "init") {
523
+ const template = (args.match(/--template[= ](\S+)/)?.[1] as
524
+ | "minimal" | "web-app" | "library" | "cli" | undefined) ?? undefined;
525
+ const autoYes = args.includes("--yes");
526
+ const projectName = args.match(/--name[= ](\S+)/)?.[1];
527
+ const initUi = {
528
+ notify: (t: string, k?: "info" | "warning" | "error") => ctx.ui.notify(t, k ?? "info"),
529
+ select: async (label: string, options: string[]) => ctx.ui.select(label, options),
530
+ confirm: (title: string, message: string) => ctx.ui.confirm(title, message),
531
+ input: (label: string, placeholder?: string) => ctx.ui.input(label, placeholder),
532
+ };
533
+ await initSolyProject(ctx.cwd, initUi, { template, autoYes, projectName });
534
+ return;
535
+ }
536
+ }
537
+
567
538
  const state = getState();
568
539
  if (!state.exists) {
569
- ui.notify("soly: no .soly/ directory in cwd", "error");
540
+ ui.notify("soly: no .agents/ project here — run `/soly init` to scaffold one", "info");
570
541
  return;
571
542
  }
572
543
 
@@ -592,7 +563,7 @@ What must the LLM do?
592
563
  // extension (rotor switcher removed in 1.4.0).
593
564
  // Soly no longer owns a rotor switcher.
594
565
  config: {
595
- description: "show merged config (per-project + global + defaults); edit .soly/config.json or ~/.soly/config.json",
566
+ description: "show merged config (per-project + global + defaults); edit .agents/soly.json or ~/.agents/soly.json",
596
567
  run: () => {
597
568
  const cfg = getConfig();
598
569
  const out: string[] = [];
@@ -603,12 +574,12 @@ What must the LLM do?
603
574
  out.push("```");
604
575
  out.push("");
605
576
  out.push("Sources:");
606
- out.push(` global: ~/.soly/config.json`);
607
- out.push(` project: <cwd>/.soly/config.json`);
577
+ out.push(` global: ~/.agents/soly.json`);
578
+ out.push(` project: <cwd>/.agents/soly.json`);
608
579
  out.push("");
609
580
  out.push("To edit:");
610
- out.push(` - project: edit \`${state.solyDir}/config.json\` directly`);
611
- out.push(` - global: edit \`~/.soly/config.json\``);
581
+ out.push(` - project: edit \`${state.solyDir}/soly.json\` directly`);
582
+ out.push(` - global: edit \`~/.agents/soly.json\``);
612
583
  out.push("After editing, run /soly reload to re-pick up changes.");
613
584
  ui.notify(out.join("\n"), "info");
614
585
  },
@@ -720,7 +691,7 @@ What must the LLM do?
720
691
  run: () => {
721
692
  const s = getState();
722
693
  if (s.tasks.length === 0) {
723
- ui.notify("soly: no tasks found in .soly/features/*/tasks/", "info");
694
+ ui.notify("soly: no tasks found in .agents/features/*/tasks/", "info");
724
695
  return;
725
696
  }
726
697
  const byFeature = new Map<string, typeof s.tasks>();
@@ -779,7 +750,7 @@ What must the LLM do?
779
750
  run: () => {
780
751
  const features = getState().features;
781
752
  if (features.length === 0) {
782
- ui.notify("soly: no features found in .soly/features/", "info");
753
+ ui.notify("soly: no features found in .agents/features/", "info");
783
754
  return;
784
755
  }
785
756
  const lines = features.map((f) => {
@@ -790,7 +761,7 @@ What must the LLM do?
790
761
  },
791
762
  },
792
763
  milestone: {
793
- description: "show the active milestone document (.soly/milestones/<v>.md)",
764
+ description: "show the active milestone document (.agents/milestones/<v>.md)",
794
765
  run: () => {
795
766
  const s = getState();
796
767
  if (!s.milestone || s.milestone === "—") {
@@ -996,7 +967,7 @@ What must the LLM do?
996
967
  "",
997
968
  "tell me what behavior or outcome you want to constrain. I'll help you",
998
969
  "decide whether it should be:",
999
- " • a soly rule (.soly/rules/*.md) — for process, behavior, or project",
970
+ " • a soly rule (.agents/rules/*.md) — for process, behavior, or project",
1000
971
  " conventions the LLM must follow",
1001
972
  " • an .editorconfig entry — for formatting (indent, line endings, EOL,",
1002
973
  " charset, trailing whitespace, max line length)",
package/config.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // config.ts — soly config loader (per-project + global)
3
3
  // =============================================================================
4
4
  //
5
- // Per-project: `.soly/config.json` (version-controlled, repo-specific).
6
- // Global: `~/.soly/config.json` (per-user, applies to all projects).
5
+ // Per-project: `.agents/soly.json` (version-controlled, repo-specific).
6
+ // Global: `~/.agents/soly.json` (per-user, applies to all projects).
7
7
  //
8
8
  // Lookup order: per-project overrides global overrides defaults. Missing
9
9
  // files are silently ignored (defaults apply). Malformed JSON returns a
@@ -41,10 +41,14 @@ export interface SolyConfig {
41
41
  /** Inject per-turn affordance hints (examples → html_artifact, options →
42
42
  * decision_deck, …) when the prompt mentions those. Default on. */
43
43
  toolHints: boolean;
44
- /** For non-trivial tasks, tell the LLM to confirm the approach with the
45
- * user (ask "ready to implement, or discuss first?") before writing code.
46
- * Default on. */
47
- confirmBeforeCode: boolean;
44
+ /** Before non-trivial coding, make the LLM ask the user the decisions
45
+ * only they can make, instead of coding on assumptions. Levels:
46
+ * "scope" batch the substantive questions (placement, pattern,
47
+ * scope, interface) via ask_pro, then wait. (default)
48
+ * "ask" — lighter: one "ready to implement, or discuss?" question.
49
+ * "off" — no gate.
50
+ * Booleans accepted for back-compat: true = "scope", false = "off". */
51
+ confirmBeforeCode: boolean | "off" | "ask" | "scope";
48
52
  /** Inject a compact list of rules applicable to the file being
49
53
  * edited/written, appended to the tool's result. Forces the LLM to
50
54
  * confirm in the next message which rules were applied — closes the
@@ -111,7 +115,7 @@ export interface SolyConfig {
111
115
  * (one stable localhost URL). When false, just open the .html file. */
112
116
  server: boolean;
113
117
  /** Path to a CSS file overriding the artifact theme. Empty → built-in.
114
- * Resolved like `dir`; also auto-detected at `.soly/artifact-theme.css`. */
118
+ * Resolved like `dir`; also auto-detected at `.agents/artifact-theme.css`. */
115
119
  theme: string;
116
120
  /** Prune session artifact dirs older than N days on session start.
117
121
  * 0 = keep forever. */
@@ -131,7 +135,7 @@ export const DEFAULT_CONFIG: SolyConfig = {
131
135
  autoCheckpointOnPause: true,
132
136
  nudgeNotify: false,
133
137
  toolHints: true,
134
- confirmBeforeCode: true,
138
+ confirmBeforeCode: "scope",
135
139
  preActionRuleReminder: true,
136
140
  },
137
141
  display: {
@@ -222,7 +226,12 @@ function deepMerge(base: SolyConfig, over: RawConfig): SolyConfig {
222
226
  merged.agent.nudgeNotify = over.agent.nudgeNotify;
223
227
  if (typeof over.agent.toolHints === "boolean")
224
228
  merged.agent.toolHints = over.agent.toolHints;
225
- if (typeof over.agent.confirmBeforeCode === "boolean")
229
+ if (
230
+ typeof over.agent.confirmBeforeCode === "boolean" ||
231
+ over.agent.confirmBeforeCode === "off" ||
232
+ over.agent.confirmBeforeCode === "ask" ||
233
+ over.agent.confirmBeforeCode === "scope"
234
+ )
226
235
  merged.agent.confirmBeforeCode = over.agent.confirmBeforeCode;
227
236
  if (typeof over.agent.preActionRuleReminder === "boolean")
228
237
  merged.agent.preActionRuleReminder = over.agent.preActionRuleReminder;
@@ -291,7 +300,7 @@ export function loadConfig(cwd: string, homeDir?: string): LoadConfigResult {
291
300
  const sources = { global: null as string | null, project: null as string | null };
292
301
 
293
302
  const home = homeDir ?? os.homedir();
294
- const globalPath = path.join(home, ".soly", "config.json");
303
+ const globalPath = path.join(home, ".agents", "soly.json");
295
304
  const globalRaw = readJsonIfExists(globalPath);
296
305
  if (globalRaw) {
297
306
  sources.global = globalPath;
@@ -301,8 +310,7 @@ export function loadConfig(cwd: string, homeDir?: string): LoadConfigResult {
301
310
  );
302
311
  }
303
312
  }
304
- const projectSolyDir = path.join(cwd, ".soly");
305
- const projectPath = path.join(projectSolyDir, "config.json");
313
+ const projectPath = path.join(cwd, ".agents", "soly.json");
306
314
  const projectRaw = readJsonIfExists(projectPath);
307
315
  if (projectRaw) {
308
316
  sources.project = projectPath;
package/core.ts CHANGED
@@ -3,12 +3,12 @@
3
3
  // =============================================================================
4
4
  //
5
5
  // Owns:
6
- // - Rule loading from .soly/rules/ (project + global)
7
- // - Soly project state loading from .soly/ (STATE.md, ROADMAP.md, phases/)
6
+ // - Rule loading from .agents/rules/ (project + global)
7
+ // - Soly project state loading from .agents/ (STATE.md, ROADMAP.md, phases/)
8
8
  // - Status line construction
9
9
  // - Shared utility functions and constants
10
10
  //
11
- // Path convention: <cwd>/.soly/. Pi itself loads AGENTS.md / CLAUDE.md
11
+ // Path convention: <cwd>/.agents/. Pi itself loads AGENTS.md / CLAUDE.md
12
12
  // from ancestor directories through its own resource loader, so soly
13
13
  // stays out of that path.
14
14
  // =============================================================================
@@ -142,8 +142,8 @@ export interface PhaseInfo {
142
142
 
143
143
  /**
144
144
  * A feature is a logical grouping of tasks (e.g. "auth", "orders").
145
- * Dual-mode with phases: features live under `.soly/features/`, phases
146
- * under `.soly/phases/`. soly supports both simultaneously.
145
+ * Dual-mode with phases: features live under `.agents/features/`, phases
146
+ * under `.agents/phases/`. soly supports both simultaneously.
147
147
  */
148
148
  export interface FeatureInfo {
149
149
  name: string;
@@ -548,7 +548,7 @@ export function buildRulesSection(
548
548
  headerHint = `Phase ${phase} rules are loaded for the currently active phase; all other rules are always-on. Inline @see references are resolved recursively.`;
549
549
  } else {
550
550
  blocks = applicable.map(render);
551
- headerHint = `The following rules are loaded from \`.soly/rules/\` and \`~/.soly/rules/\` and are mandatory. Follow them strictly. Inline @see references are resolved recursively.`;
551
+ headerHint = `The following rules are loaded from \`.agents/rules/\` and \`~/.agents/rules/\` and are mandatory. Follow them strictly. Inline @see references are resolved recursively.`;
552
552
  }
553
553
 
554
554
  const skippedNote = skipped.length
@@ -981,7 +981,7 @@ export function formatRulesContextStats(stats: RulesContextStats): string {
981
981
  // @import resolver (markdown only)
982
982
  // ============================================================================
983
983
  //
984
- // Used by intent docs (`.soly/docs/*.md`) to inline other markdown files
984
+ // Used by intent docs (`.agents/docs/*.md`) to inline other markdown files
985
985
  // via `@import path/to/file.md` lines. Cycles and > MAX_IMPORT_DEPTH
986
986
  // are skipped with a comment.
987
987
  //
@@ -1121,7 +1121,7 @@ export function resolveImports(
1121
1121
  }
1122
1122
 
1123
1123
  // ============================================================================
1124
- // Project state (.soly/)
1124
+ // Project state (.agents/)
1125
1125
  // ============================================================================
1126
1126
 
1127
1127
  function extractCurrentPosition(body: string): SolyPosition | null {
@@ -1524,20 +1524,18 @@ export function buildStatusLine(
1524
1524
  /** Preferred soly dir name (vendor-neutral). */
1525
1525
  export const SOLY_DIRNAME = ".agents";
1526
1526
 
1527
- /** Legacy soly dir name. Kept for backward compat with existing projects. */
1527
+ /** Legacy soly dir name. Detection-only soly no longer reads or writes it.
1528
+ * Used to warn users with an old `.soly/` project to rename it to `.agents/`. */
1528
1529
  export const LEGACY_SOLY_DIRNAME = ".soly";
1529
1530
 
1530
- /** Which project subdir name is currently in use. Returns the first
1531
- * one that exists, preferring `.agents/`. Falls back to `.soly/` if
1532
- * no `.agents/` exists. If neither exists, returns `.agents/` (so
1533
- * new writes go to the new location). */
1531
+ /** The soly project dir for a given cwd. Always `<cwd>/.agents/` — the legacy
1532
+ * `.soly/` location is no longer read (rename it; `isLegacySolyDir` warns). */
1534
1533
  export function solyDirFor(cwd: string): string {
1535
- if (fs.existsSync(path.join(cwd, SOLY_DIRNAME))) return path.join(cwd, SOLY_DIRNAME);
1536
- if (fs.existsSync(path.join(cwd, LEGACY_SOLY_DIRNAME))) return path.join(cwd, LEGACY_SOLY_DIRNAME);
1537
- return path.join(cwd, SOLY_DIRNAME); // default to new for new projects
1534
+ return path.join(cwd, SOLY_DIRNAME);
1538
1535
  }
1539
1536
 
1540
- /** True if the legacy `.soly/` dir is in active use (and `.agents/` isn't). */
1537
+ /** True if a legacy `.soly/` dir exists but `.agents/` doesn't — i.e. the
1538
+ * project predates the rename and is now invisible to soly. Warn the user. */
1541
1539
  export function isLegacySolyDir(cwd: string): boolean {
1542
1540
  const newPath = path.join(cwd, SOLY_DIRNAME);
1543
1541
  const oldPath = path.join(cwd, LEGACY_SOLY_DIRNAME);
@@ -1553,7 +1551,7 @@ export function isLegacySolyDir(cwd: string): boolean {
1553
1551
  // the next sensible soly action without needing to read STATE.md.
1554
1552
  //
1555
1553
  // Returns null when:
1556
- // - there is no .soly/ in cwd (nothing to suggest)
1554
+ // - there is no .agents/ in cwd (nothing to suggest)
1557
1555
  // - every phase is already complete
1558
1556
  //
1559
1557
  // Heuristic priority (first match wins):
package/docs.ts CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
- import { estimateTokens, findMarkdownFiles, readIfExists } from "./core.ts";
13
+ import { estimateTokens, findMarkdownFiles, readIfExists, solyDirFor } from "./core.ts";
14
14
  import { extractTitleAndPreview, stripHtml } from "./html.ts";
15
15
 
16
16
  // Re-export the stripHtml helper so existing imports of `stripHtml from
@@ -42,7 +42,7 @@ const DOC_GLOBS_IGNORE = [
42
42
  "venv",
43
43
  ];
44
44
 
45
- // .soly/ IS indexed, but intent docs in .soly/docs/ and .soly/phases/<N>/docs/
45
+ // .agents/ IS indexed, but intent docs in .agents/docs/ and .agents/phases/<N>/docs/
46
46
  // are tagged with higher priority in search results (see buildDocIndex).
47
47
 
48
48
  const INTENT_DOC_EXTS = [".md", ".html", ".htm"];
@@ -72,7 +72,7 @@ export function buildDocIndex(cwd: string, limit = 5000): DocIndexEntry[] {
72
72
  };
73
73
 
74
74
  // 1. Intent docs (priority 0)
75
- const docsRoot = path.join(cwd, ".soly", "docs");
75
+ const docsRoot = path.join(solyDirFor(cwd), "docs");
76
76
  if (fs.existsSync(docsRoot)) {
77
77
  const intentFiles = findIntentFiles(docsRoot);
78
78
  for (const f of intentFiles) {
@@ -83,7 +83,7 @@ export function buildDocIndex(cwd: string, limit = 5000): DocIndexEntry[] {
83
83
  }
84
84
 
85
85
  // 2. Phase intent docs (priority 1) — only if phases dir exists
86
- const phasesRoot = path.join(cwd, ".soly", "phases");
86
+ const phasesRoot = path.join(solyDirFor(cwd), "phases");
87
87
  if (out.length < limit && fs.existsSync(phasesRoot)) {
88
88
  try {
89
89
  const phaseEntries = fs.readdirSync(phasesRoot, { withFileTypes: true });