pi-soly 1.13.4 β 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 +9 -10
- package/artifact/index.ts +3 -3
- package/artifact/render.ts +2 -2
- package/ask/README.md +3 -1
- package/ask/index.ts +16 -2
- package/codemap.ts +2 -2
- package/commands.ts +40 -67
- package/config.ts +20 -12
- package/core.ts +16 -18
- package/docs.ts +4 -4
- package/index.ts +39 -29
- package/init.ts +7 -7
- package/intent.ts +12 -12
- package/iteration.ts +12 -12
- package/mcp/ext-apps-bridge.ts +76 -0
- package/mcp/index.ts +5 -0
- package/mcp/metadata-cache.ts +1 -1
- package/mcp/tool-metadata.ts +1 -1
- package/mcp/ui-resource-handler.ts +4 -4
- package/mcp/ui-server.ts +1 -1
- package/notification.ts +1 -1
- package/notifications-log.ts +2 -2
- package/nudge.ts +41 -6
- package/package.json +1 -2
- package/skills/soly-framework/SKILL.md +34 -42
- package/status.ts +2 -2
- package/tools.ts +14 -14
- package/util.ts +2 -2
- package/visual/welcome.ts +3 -3
- package/workflows/execute.ts +16 -16
- package/workflows/index.ts +4 -14
- package/workflows/inspect.ts +16 -15
- package/workflows/parser.ts +2 -3
- package/workflows/pause.ts +5 -5
- package/workflows/planning.ts +18 -18
- package/workflows/quick.ts +7 -7
- package/workflows/resume.ts +14 -14
- package/workflows-data/discuss-phase.md +9 -9
- package/workflows-data/execute-phase.md +4 -4
- package/workflows-data/execute-plan.md +8 -8
- package/workflows-data/execute-task.md +7 -7
- package/workflows-data/pause-work.md +18 -18
- package/workflows-data/plan-phase.md +7 -7
- package/workflows-data/plan-task.md +18 -18
- package/migrate.ts +0 -258
- package/workflows/migrate.ts +0 -85
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ soly status # current position + progress (no LLM round-trip)
|
|
|
89
89
|
### State inspection (`/soly`)
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
/soly # interactive picker (
|
|
92
|
+
/soly # interactive modal picker (live preview per item, β to open)
|
|
93
93
|
/soly position # where am I in the plan
|
|
94
94
|
/soly state # current STATE.md content
|
|
95
95
|
/soly roadmap # all phases
|
|
@@ -114,8 +114,7 @@ soly status # current position + progress (no LLM round-trip)
|
|
|
114
114
|
### Setup
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
/soly
|
|
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 β `.
|
|
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 β `.
|
|
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 `.
|
|
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
|
-
.
|
|
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 `.
|
|
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
|
-
.
|
|
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
|
|
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 β .
|
|
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, ".
|
|
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 .
|
|
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({
|
package/artifact/render.ts
CHANGED
|
@@ -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 `.
|
|
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 .
|
|
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β¦"** β
|
|
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`), `
|
|
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({
|
|
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, .
|
|
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
|
-
".
|
|
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 .
|
|
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 .
|
|
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 .
|
|
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
|
|
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
|
|
607
|
-
out.push(` project: <cwd>/.soly
|
|
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}/
|
|
611
|
-
out.push(` - global: edit \`~/.soly
|
|
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 .
|
|
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 .
|
|
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 (.
|
|
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 === "β") {
|
|
@@ -828,11 +799,13 @@ What must the LLM do?
|
|
|
828
799
|
},
|
|
829
800
|
};
|
|
830
801
|
|
|
802
|
+
// Single-width BMP glyphs only β astral/VS16 emoji are mis-measured by
|
|
803
|
+
// visibleWidth, overflow the panel, and ghost the modal on ββ.
|
|
831
804
|
const ICONS: Record<string, string> = {
|
|
832
|
-
position: "
|
|
833
|
-
research: "
|
|
834
|
-
phases: "
|
|
835
|
-
features: "
|
|
805
|
+
position: "β", state: "β€", plan: "β₯", context: "β",
|
|
806
|
+
research: "β", roadmap: "β£", progress: "β°",
|
|
807
|
+
phases: "β", tasks: "β", task: "β",
|
|
808
|
+
features: "β
", milestone: "β", reload: "β»", config: "β£",
|
|
836
809
|
};
|
|
837
810
|
|
|
838
811
|
// Short live preview shown in the modal's preview pane per subcommand.
|
|
@@ -994,7 +967,7 @@ What must the LLM do?
|
|
|
994
967
|
"",
|
|
995
968
|
"tell me what behavior or outcome you want to constrain. I'll help you",
|
|
996
969
|
"decide whether it should be:",
|
|
997
|
-
" β’ a soly rule (.
|
|
970
|
+
" β’ a soly rule (.agents/rules/*.md) β for process, behavior, or project",
|
|
998
971
|
" conventions the LLM must follow",
|
|
999
972
|
" β’ an .editorconfig entry β for formatting (indent, line endings, EOL,",
|
|
1000
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
|
|
6
|
-
// Global: `~/.soly
|
|
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
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
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 `.
|
|
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:
|
|
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 (
|
|
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, ".
|
|
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
|
|
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 .
|
|
7
|
-
// - Soly project state loading from .
|
|
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>/.
|
|
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 `.
|
|
146
|
-
* under `.
|
|
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 \`.
|
|
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 (`.
|
|
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 (.
|
|
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.
|
|
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
|
-
/**
|
|
1531
|
-
*
|
|
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
|
-
|
|
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
|
|
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 .
|
|
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
|
-
// .
|
|
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, "
|
|
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, "
|
|
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 });
|