pi-soly 2.2.7 → 2.3.2
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/commands/_helpers.ts +3 -0
- package/commands/artifacts.ts +8 -8
- package/commands/docs.ts +6 -6
- package/commands/rules.ts +17 -17
- package/commands/rulewizard.ts +5 -2
- package/commands/soly.ts +28 -28
- package/commands/why.ts +3 -3
- package/commands.ts +1 -1
- package/index.ts +19 -11
- package/init.ts +2 -1
- package/mcp/commands.ts +18 -17
- package/mcp/elicitation-handler.ts +2 -1
- package/mcp/index.ts +6 -5
- package/mcp/init.ts +5 -4
- package/mcp/notify.ts +2 -1
- package/notification.ts +2 -1
- package/package.json +1 -1
- package/visual/chrome.ts +17 -1
- package/visual/data.ts +8 -0
- package/visual/event-sink.ts +30 -0
- package/visual/footer.ts +19 -1
- package/workflows/done.ts +1 -1
- package/workflows/index.ts +5 -3
- package/workflows/inspect.ts +14 -13
- package/workflows/new.ts +36 -8
- package/workflows/parser.ts +93 -47
- package/workflows/quick.ts +5 -4
- package/workflows/settings-ui.ts +3 -2
- package/workflows/verify.ts +6 -7
package/commands/_helpers.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface CommandsDeps {
|
|
|
40
40
|
getConfig: () => SolyConfig;
|
|
41
41
|
reloadConfig: () => void;
|
|
42
42
|
getIntentDocs: () => IntentDoc[];
|
|
43
|
+
/** Record a soly event (any level). Everything goes through the
|
|
44
|
+
* Working sub-line — no popups at all. */
|
|
45
|
+
recordEvent: (text: string, level?: "info" | "warning" | "error") => void;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/** Open a focused list modal (overlay) for the given grouped items. */
|
package/commands/artifacts.ts
CHANGED
|
@@ -6,16 +6,16 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
|
|
|
6
6
|
import { getArtifactServer, ensureArtifactServer, artifactDir } from "../artifact/session.ts";
|
|
7
7
|
import { openListPanel, openExternally, type CommandsDeps } from "./_helpers.ts";
|
|
8
8
|
|
|
9
|
-
type ArtifactsDeps = Pick<CommandsDeps, "getConfig">;
|
|
9
|
+
type ArtifactsDeps = Pick<CommandsDeps, "getConfig" | "recordEvent">;
|
|
10
10
|
|
|
11
11
|
export function registerArtifactsCommand(pi: ExtensionAPI, deps: ArtifactsDeps): void {
|
|
12
|
-
const { getConfig } = deps;
|
|
12
|
+
const { getConfig, recordEvent } = deps;
|
|
13
13
|
|
|
14
14
|
pi.registerCommand("artifacts", {
|
|
15
15
|
description: "browse this project's html_artifact gallery (list, open, clear)",
|
|
16
16
|
handler: async (args, ctx) => {
|
|
17
17
|
if (!getConfig().artifacts.server) {
|
|
18
|
-
|
|
18
|
+
recordEvent("artifact server is disabled (artifacts.server=false)");
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
// Always re-resolve: reuse another window's server or start ours, and
|
|
@@ -27,7 +27,7 @@ export function registerArtifactsCommand(pi: ExtensionAPI, deps: ArtifactsDeps):
|
|
|
27
27
|
// ignore — handled by the count check below
|
|
28
28
|
}
|
|
29
29
|
if (!server || server.count === 0) {
|
|
30
|
-
|
|
30
|
+
recordEvent("no artifacts for this project yet (use the html_artifact tool)");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
33
|
const gallery = server.galleryUrl();
|
|
@@ -35,15 +35,15 @@ export function registerArtifactsCommand(pi: ExtensionAPI, deps: ArtifactsDeps):
|
|
|
35
35
|
|
|
36
36
|
if (sub === "clear") {
|
|
37
37
|
const n = server.clear();
|
|
38
|
-
|
|
38
|
+
recordEvent(`cleared ${n} artifact(s)`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
if (sub === "open" || sub === "gallery") {
|
|
42
42
|
try {
|
|
43
43
|
await openExternally(pi, gallery);
|
|
44
|
-
|
|
44
|
+
recordEvent(`opened ${gallery}`);
|
|
45
45
|
} catch {
|
|
46
|
-
|
|
46
|
+
recordEvent(`soly artifacts gallery: ${gallery}`);
|
|
47
47
|
}
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
@@ -80,7 +80,7 @@ export function registerArtifactsCommand(pi: ExtensionAPI, deps: ArtifactsDeps):
|
|
|
80
80
|
|
|
81
81
|
// Non-TUI: print the list + gallery URL.
|
|
82
82
|
const lines = server.list().map((a) => `🖼 ${a.title} — ${a.url}`);
|
|
83
|
-
|
|
83
|
+
recordEvent([`soly artifacts gallery: ${gallery}`, "", ...lines].join("\n"));
|
|
84
84
|
},
|
|
85
85
|
});
|
|
86
86
|
}
|
package/commands/docs.ts
CHANGED
|
@@ -7,10 +7,10 @@ import { formatTok, solyDirFor } from "../core.ts";
|
|
|
7
7
|
import { buildIntentStats, formatIntentStats, loadInlineIntentBodies, type IntentInlineDoc } from "../intent.ts";
|
|
8
8
|
import { openListPanel, type CommandUI, type CommandsDeps } from "./_helpers.ts";
|
|
9
9
|
|
|
10
|
-
type DocsDeps = Pick<CommandsDeps, "getIntentDocs">;
|
|
10
|
+
type DocsDeps = Pick<CommandsDeps, "getIntentDocs" | "recordEvent">;
|
|
11
11
|
|
|
12
12
|
export function registerDocsCommand(pi: ExtensionAPI, deps: DocsDeps): void {
|
|
13
|
-
const { getIntentDocs } = deps;
|
|
13
|
+
const { getIntentDocs, recordEvent } = deps;
|
|
14
14
|
|
|
15
15
|
pi.registerCommand("docs", {
|
|
16
16
|
description: "manage soly intent docs (stats — show context breakdown)",
|
|
@@ -30,7 +30,7 @@ export function registerDocsCommand(pi: ExtensionAPI, deps: DocsDeps): void {
|
|
|
30
30
|
if (sub === "list") {
|
|
31
31
|
const docs = getIntentDocs();
|
|
32
32
|
if (docs.length === 0) {
|
|
33
|
-
|
|
33
|
+
recordEvent("no intent docs found in .agents/docs/ — drop your vision/domain docs there");
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
if (ctx.mode === "tui") {
|
|
@@ -55,7 +55,7 @@ export function registerDocsCommand(pi: ExtensionAPI, deps: DocsDeps): void {
|
|
|
55
55
|
});
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
recordEvent(docs.map((d) => `○ ${d.title || d.relPath} (${formatTok(d.tokens)} tok)`).join("\n"));
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,11 +63,11 @@ export function registerDocsCommand(pi: ExtensionAPI, deps: DocsDeps): void {
|
|
|
63
63
|
const docs = getIntentDocs();
|
|
64
64
|
const inlineBodies: IntentInlineDoc[] = loadInlineIntentBodies(docs);
|
|
65
65
|
const stats = buildIntentStats(docs, inlineBodies);
|
|
66
|
-
|
|
66
|
+
recordEvent(formatIntentStats(stats));
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
recordEvent("Usage: /docs <list|stats>", "error");
|
|
71
71
|
void solyDirFor; // kept available for future subcommands
|
|
72
72
|
},
|
|
73
73
|
});
|
package/commands/rules.ts
CHANGED
|
@@ -30,10 +30,10 @@ function ruleItem(r: RuleFile): ListItem {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
type RulesDeps = Pick<CommandsDeps, "getRules" | "getOverridden" | "refreshRules" | "updateStatus">;
|
|
33
|
+
type RulesDeps = Pick<CommandsDeps, "getRules" | "getOverridden" | "refreshRules" | "updateStatus" | "recordEvent">;
|
|
34
34
|
|
|
35
35
|
export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
36
|
-
const { getRules, getOverridden, refreshRules, updateStatus } = deps;
|
|
36
|
+
const { getRules, getOverridden, refreshRules, updateStatus, recordEvent } = deps;
|
|
37
37
|
|
|
38
38
|
pi.registerCommand("rules", {
|
|
39
39
|
description: "manage soly rules (list, show, stats, analytics, reload, enable, disable)",
|
|
@@ -55,7 +55,7 @@ export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
|
55
55
|
const rules = getRules();
|
|
56
56
|
const overridden = getOverridden();
|
|
57
57
|
if (rules.length === 0 && overridden.length === 0) {
|
|
58
|
-
|
|
58
|
+
recordEvent("no rules loaded — check `.agents/rules/` or `~/.agents/rules/`", "warning");
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
// Rich modal in the TUI; plain select elsewhere (RPC/print).
|
|
@@ -85,63 +85,63 @@ export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
|
85
85
|
for (const p of overridden) {
|
|
86
86
|
lines.push(`⊘ [overridden] ${p}`);
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
recordEvent(lines.join("\n"));
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (sub === "show") {
|
|
93
93
|
if (!target) {
|
|
94
|
-
|
|
94
|
+
recordEvent("Usage: /rules show <relPath>", "error");
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const r = getRules().find((x) => x.relPath === target);
|
|
98
98
|
if (!r) {
|
|
99
|
-
|
|
99
|
+
recordEvent(`Rule not found: ${target}`, "error");
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
const text = `---\n${r.meta.description ? `description: ${r.meta.description}\n` : ""}source: ${r.sourceLabel}\nenabled: ${r.enabled}\n---\n\n${r.body}`;
|
|
103
|
-
|
|
103
|
+
recordEvent(text);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (sub === "stats") {
|
|
108
108
|
const analytics = analyzeRules(getRules(), CONTEXT_WINDOW_TOKENS);
|
|
109
|
-
|
|
109
|
+
recordEvent(formatAnalyticsFull(analytics));
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (sub === "analytics") {
|
|
114
114
|
const rules = getRules();
|
|
115
115
|
const stats = buildRulesContextStats(rules, CONTEXT_WINDOW_TOKENS);
|
|
116
|
-
|
|
116
|
+
recordEvent(formatRulesContextStats(stats));
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
if (sub === "enable" || sub === "disable") {
|
|
121
121
|
if (!target) {
|
|
122
|
-
|
|
122
|
+
recordEvent(`Usage: /rules ${sub} <relPath>`, "error");
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
125
|
const r = getRules().find((x) => x.relPath === target);
|
|
126
126
|
if (!r) {
|
|
127
|
-
|
|
127
|
+
recordEvent(`Rule not found: ${target}`, "error");
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
r.enabled = sub === "enable";
|
|
131
131
|
updateStatus(ui);
|
|
132
|
-
|
|
132
|
+
recordEvent(`Rule ${target}: ${sub}d`);
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
if (sub === "reload") {
|
|
137
137
|
refreshRules();
|
|
138
138
|
updateStatus(ui);
|
|
139
|
-
|
|
139
|
+
recordEvent(`Reloaded ${getRules().length} rules`);
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
if (sub === "add") {
|
|
144
|
-
|
|
144
|
+
recordEvent(
|
|
145
145
|
"Use /rulewizard add <url> (or /rules add for the rule-creation guide).",
|
|
146
146
|
"info",
|
|
147
147
|
);
|
|
@@ -149,7 +149,7 @@ export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (sub === "new") {
|
|
152
|
-
|
|
152
|
+
recordEvent(
|
|
153
153
|
"Use /rulewizard to scaffold a new rule (it guides the rule-vs-editorconfig-vs-linter decision).",
|
|
154
154
|
"info",
|
|
155
155
|
);
|
|
@@ -165,7 +165,7 @@ export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
|
165
165
|
path.join(require("node:os").homedir(), ".agents", "rules"),
|
|
166
166
|
];
|
|
167
167
|
const found = dirs.filter((d) => fs.existsSync(d));
|
|
168
|
-
|
|
168
|
+
recordEvent(
|
|
169
169
|
"Rules sources (existing first):\n" +
|
|
170
170
|
(found.length ? found.map((d) => ` ✓ ${d}`).join("\n") : " (none)") +
|
|
171
171
|
"\n" +
|
|
@@ -178,7 +178,7 @@ export function registerRulesCommand(pi: ExtensionAPI, deps: RulesDeps): void {
|
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
recordEvent(
|
|
182
182
|
`Usage: /rules <list|show|stats|analytics|enable|disable|reload|add|new|path> [target]`,
|
|
183
183
|
"error",
|
|
184
184
|
);
|
package/commands/rulewizard.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
+
import { emit } from "../visual/event-sink.ts";
|
|
1
2
|
// =============================================================================
|
|
2
3
|
// commands/rulewizard.ts — /rulewizard command (interactive guide)
|
|
3
4
|
// =============================================================================
|
|
4
5
|
|
|
5
6
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { CommandsDeps } from "./_helpers.ts";
|
|
6
8
|
|
|
7
|
-
export function registerRulewizardCommand(pi: ExtensionAPI): void {
|
|
9
|
+
export function registerRulewizardCommand(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
10
|
+
void deps; // rulewizard doesn't currently emit non-error events
|
|
8
11
|
pi.registerCommand("rulewizard", {
|
|
9
12
|
description:
|
|
10
13
|
"interactive guide: decide whether a constraint should be a soly rule, an .editorconfig entry, or a linter config (eslint/biome/prettier). Use this BEFORE writing a new rule to avoid duplicating what linters already enforce.",
|
|
11
14
|
handler: async (_args, ctx) => {
|
|
12
|
-
|
|
15
|
+
emit(
|
|
13
16
|
[
|
|
14
17
|
"soly-rule-wizard:",
|
|
15
18
|
"",
|
package/commands/soly.ts
CHANGED
|
@@ -25,10 +25,10 @@ import { initSolyProject } from "../init.js";
|
|
|
25
25
|
import { parseSolyCommand, type SolyCommand, type WorkflowVerb } from "../workflows/parser.ts";
|
|
26
26
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
27
27
|
|
|
28
|
-
type SolyDeps = Pick<CommandsDeps, "getState" | "getConfig" | "reloadConfig" | "updateStatus" | "refreshState">;
|
|
28
|
+
type SolyDeps = Pick<CommandsDeps, "getState" | "getConfig" | "reloadConfig" | "updateStatus" | "refreshState" | "recordEvent">;
|
|
29
29
|
|
|
30
30
|
export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
31
|
-
const { getState, getConfig, reloadConfig, updateStatus, refreshState } = deps;
|
|
31
|
+
const { getState, getConfig, reloadConfig, updateStatus, refreshState, recordEvent } = deps;
|
|
32
32
|
|
|
33
33
|
const solyBody = async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
34
34
|
const ui: CommandUI = {
|
|
@@ -68,7 +68,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
68
68
|
|
|
69
69
|
const showFile = (label: string, content: string | null) => {
|
|
70
70
|
if (!content) {
|
|
71
|
-
|
|
71
|
+
recordEvent(`${label}: not found`, "error");
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
const MAX = 4000;
|
|
@@ -105,22 +105,22 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
105
105
|
switch (verb) {
|
|
106
106
|
case "new": {
|
|
107
107
|
const r = buildNewTransform(cmd, state, ui, ctx.cwd, getConfig().plan.defaultBranchPrefix);
|
|
108
|
-
if (r.handled && r.transformedText)
|
|
108
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
case "done": {
|
|
112
112
|
const r = buildDoneTransform(cmd, state, ui, ctx.cwd, { defaultBranchPrefix: getConfig().plan.defaultBranchPrefix });
|
|
113
|
-
if (r.handled && r.transformedText)
|
|
113
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
case "migrate": {
|
|
117
117
|
const r = buildMigrateTransform(state, ui, ctx.cwd);
|
|
118
|
-
if (r.handled && r.transformedText)
|
|
118
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
case "plan": {
|
|
122
122
|
const r = buildPlanTransform(cmd, state);
|
|
123
|
-
if (r.handled && r.transformedText)
|
|
123
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
case "discuss": {
|
|
@@ -129,13 +129,13 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
129
129
|
// plain-text discussion format; the LLM-driven path still
|
|
130
130
|
// gets the richer ask_pro flow.
|
|
131
131
|
const r = buildDiscussTransform(cmd, state, { hasAskPro: false });
|
|
132
|
-
if (r.handled && r.transformedText)
|
|
132
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
case "execute": {
|
|
136
136
|
// Slash-command path doesn't have live interactive rules.
|
|
137
137
|
const r = buildExecuteTransform(cmd, state, []);
|
|
138
|
-
if (r.handled && r.transformedText)
|
|
138
|
+
if (r.handled && r.transformedText) recordEvent(r.transformedText);
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
default:
|
|
@@ -176,12 +176,12 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
176
176
|
const s = getState();
|
|
177
177
|
if (s.position) {
|
|
178
178
|
// Brief status; full inspector lives in `/soly inspect`.
|
|
179
|
-
|
|
179
|
+
recordEvent(
|
|
180
180
|
`pos: ${s.position.phase} · ${s.position.plan} (${s.position.status}) · ${s.progress.percent}%`,
|
|
181
181
|
"info",
|
|
182
182
|
);
|
|
183
183
|
} else {
|
|
184
|
-
|
|
184
|
+
recordEvent(`${s.milestone} — no position set`);
|
|
185
185
|
}
|
|
186
186
|
},
|
|
187
187
|
},
|
|
@@ -199,7 +199,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
199
199
|
if (parts.length <= 1) {
|
|
200
200
|
const s = getState();
|
|
201
201
|
if (!s.currentPlanPath) {
|
|
202
|
-
|
|
202
|
+
recordEvent("soly: no current plan", "error");
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
205
|
showFile(
|
|
@@ -216,7 +216,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
216
216
|
run: () => {
|
|
217
217
|
const s = getState();
|
|
218
218
|
if (!s.currentPhase) {
|
|
219
|
-
|
|
219
|
+
recordEvent("soly: no current phase", "error");
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
222
|
const p = path.join(s.currentPhase.dir, `${s.currentPhase.slug}-CONTEXT.md`);
|
|
@@ -228,7 +228,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
228
228
|
run: () => {
|
|
229
229
|
const s = getState();
|
|
230
230
|
if (!s.currentPhase) {
|
|
231
|
-
|
|
231
|
+
recordEvent("soly: no current phase", "error");
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
const p = path.join(s.currentPhase.dir, `${s.currentPhase.slug}-RESEARCH.md`);
|
|
@@ -244,7 +244,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
244
244
|
run: () => {
|
|
245
245
|
const s = getState();
|
|
246
246
|
const line = `${s.progress.percent}% · ${s.progress.completedPhases}/${s.progress.totalPhases} phases · ${s.progress.completedPlans}/${s.progress.totalPlans} plans`;
|
|
247
|
-
|
|
247
|
+
recordEvent(line);
|
|
248
248
|
},
|
|
249
249
|
},
|
|
250
250
|
phases: {
|
|
@@ -252,7 +252,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
252
252
|
run: () => {
|
|
253
253
|
const phases = getState().phases;
|
|
254
254
|
if (phases.length === 0) {
|
|
255
|
-
|
|
255
|
+
recordEvent("no phases");
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
258
|
const current = getState().currentPhase?.number;
|
|
@@ -261,7 +261,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
261
261
|
const cr = (p.contextExists ? "C" : "·") + (p.researchExists ? "R" : "·");
|
|
262
262
|
return `${marker} ${String(p.number).padStart(2, "0")}. ${p.name} [${cr}] plans=${p.planCount}`;
|
|
263
263
|
});
|
|
264
|
-
|
|
264
|
+
recordEvent(lines.join("\n"));
|
|
265
265
|
},
|
|
266
266
|
},
|
|
267
267
|
tasks: {
|
|
@@ -269,7 +269,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
269
269
|
run: () => {
|
|
270
270
|
const s = getState();
|
|
271
271
|
if (s.tasks.length === 0) {
|
|
272
|
-
|
|
272
|
+
recordEvent("no tasks");
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
const byFeature = new Map<string, typeof s.tasks>();
|
|
@@ -288,7 +288,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
288
288
|
}
|
|
289
289
|
out.push("");
|
|
290
290
|
}
|
|
291
|
-
|
|
291
|
+
recordEvent(out.join("\n"));
|
|
292
292
|
},
|
|
293
293
|
},
|
|
294
294
|
task: {
|
|
@@ -296,13 +296,13 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
296
296
|
run: (parts: string[]) => {
|
|
297
297
|
const id = (parts[1] ?? "").trim();
|
|
298
298
|
if (!id) {
|
|
299
|
-
|
|
299
|
+
recordEvent("Usage: /soly task <task-id>", "error");
|
|
300
300
|
return;
|
|
301
301
|
}
|
|
302
302
|
const s = getState();
|
|
303
303
|
const task = s.tasks.find((t) => t.id === id);
|
|
304
304
|
if (!task) {
|
|
305
|
-
|
|
305
|
+
recordEvent(
|
|
306
306
|
`soly: task ${id} not found.\nKnown: ${s.tasks.map((t) => t.id).join(", ") || "(none)"}`,
|
|
307
307
|
"error",
|
|
308
308
|
);
|
|
@@ -319,7 +319,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
319
319
|
if (summaryBody) {
|
|
320
320
|
showFile("SUMMARY.md", summaryBody);
|
|
321
321
|
} else {
|
|
322
|
-
|
|
322
|
+
recordEvent("no SUMMARY.md yet");
|
|
323
323
|
}
|
|
324
324
|
},
|
|
325
325
|
},
|
|
@@ -328,14 +328,14 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
328
328
|
run: () => {
|
|
329
329
|
const features = getState().features;
|
|
330
330
|
if (features.length === 0) {
|
|
331
|
-
|
|
331
|
+
recordEvent("no features");
|
|
332
332
|
return;
|
|
333
333
|
}
|
|
334
334
|
const lines = features.map((f) => {
|
|
335
335
|
const rm = f.readmeExists ? "R" : "·";
|
|
336
336
|
return ` ${f.name.padEnd(28)} tasks=${f.taskCount} [${rm}]`;
|
|
337
337
|
});
|
|
338
|
-
|
|
338
|
+
recordEvent(lines.join("\n"));
|
|
339
339
|
},
|
|
340
340
|
},
|
|
341
341
|
milestone: {
|
|
@@ -343,7 +343,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
343
343
|
run: () => {
|
|
344
344
|
const s = getState();
|
|
345
345
|
if (!s.milestone || s.milestone === "—") {
|
|
346
|
-
|
|
346
|
+
recordEvent("no milestone set");
|
|
347
347
|
return;
|
|
348
348
|
}
|
|
349
349
|
const candidates = [
|
|
@@ -357,7 +357,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
357
357
|
return;
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
|
-
|
|
360
|
+
recordEvent(
|
|
361
361
|
`soly: no milestone file found. tried:\n ${candidates.map((c) => path.relative(process.cwd(), c)).join("\n ")}`,
|
|
362
362
|
"error",
|
|
363
363
|
);
|
|
@@ -369,7 +369,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
369
369
|
refreshState();
|
|
370
370
|
updateStatus(ui);
|
|
371
371
|
const s = getState();
|
|
372
|
-
|
|
372
|
+
recordEvent(`re-read state: ${s.phases.length} phases · ${s.tasks.length} tasks`);
|
|
373
373
|
},
|
|
374
374
|
},
|
|
375
375
|
// ------------------------------------------------------------------
|
|
@@ -536,7 +536,7 @@ export function registerSolyCommand(pi: ExtensionAPI, deps: SolyDeps): void {
|
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
if (!(subcommands as Record<string, unknown>)[sub]) {
|
|
539
|
-
|
|
539
|
+
recordEvent(`soly: unknown subcommand '${sub}'`, "error");
|
|
540
540
|
return openMenu();
|
|
541
541
|
}
|
|
542
542
|
|
package/commands/why.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import type { CommandsDeps } from "./_helpers.ts";
|
|
7
7
|
|
|
8
|
-
type WhyDeps = Pick<CommandsDeps, "getState" | "getRules">;
|
|
8
|
+
type WhyDeps = Pick<CommandsDeps, "getState" | "getRules" | "recordEvent">;
|
|
9
9
|
|
|
10
10
|
export function registerWhyCommand(pi: ExtensionAPI, deps: WhyDeps): void {
|
|
11
|
-
const { getState, getRules } = deps;
|
|
11
|
+
const { getState, getRules, recordEvent } = deps;
|
|
12
12
|
|
|
13
13
|
pi.registerCommand("why", {
|
|
14
14
|
description:
|
|
@@ -97,7 +97,7 @@ export function registerWhyCommand(pi: ExtensionAPI, deps: WhyDeps): void {
|
|
|
97
97
|
"If a behavior surprises you, look here first for the basis.",
|
|
98
98
|
);
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
recordEvent(lines.join("\n"), "info");
|
|
101
101
|
|
|
102
102
|
// Suppress unused arg
|
|
103
103
|
void args;
|
package/commands.ts
CHANGED
|
@@ -39,6 +39,6 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
39
39
|
registerDocsCommand(pi, deps);
|
|
40
40
|
registerSolyCommand(pi, deps); // also registers /sly and /s
|
|
41
41
|
registerArtifactsCommand(pi, deps);
|
|
42
|
-
registerRulewizardCommand(pi);
|
|
42
|
+
registerRulewizardCommand(pi, deps);
|
|
43
43
|
registerWhyCommand(pi, deps);
|
|
44
44
|
}
|
package/index.ts
CHANGED
|
@@ -77,6 +77,7 @@ import piAskExtension from "./ask/index.ts";
|
|
|
77
77
|
import piDeckExtension from "./deck/index.ts";
|
|
78
78
|
import piArtifactExtension from "./artifact/index.ts";
|
|
79
79
|
import { getArtifactServer } from "./artifact/session.ts";
|
|
80
|
+
import { emit } from "./visual/event-sink.ts";
|
|
80
81
|
|
|
81
82
|
/** Compact phase label for the chrome top bar, e.g. "plan 2/5". Null when idle. */
|
|
82
83
|
function phaseLabelFromState(s: SolyState): string | null {
|
|
@@ -384,6 +385,10 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
384
385
|
}
|
|
385
386
|
},
|
|
386
387
|
getIntentDocs: () => intentDocs,
|
|
388
|
+
// Non-error soly events land in the Working sub-line (└─ prefixed).
|
|
389
|
+
// The route goes through the chrome so the sub-line auto-clears on
|
|
390
|
+
// the next agent_start.
|
|
391
|
+
recordEvent: (text, level) => chrome.emit(text, level),
|
|
387
392
|
});
|
|
388
393
|
|
|
389
394
|
registerTools(pi, {
|
|
@@ -393,6 +398,7 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
393
398
|
});
|
|
394
399
|
|
|
395
400
|
registerWorkflows(pi, {
|
|
401
|
+
recordEvent: (text, level) => chrome.emit(text, level),
|
|
396
402
|
getState: () => state,
|
|
397
403
|
getInteractiveRules: () =>
|
|
398
404
|
combinedRules()
|
|
@@ -458,20 +464,20 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
458
464
|
const cfgResult = loadConfig(ctx.cwd);
|
|
459
465
|
activeConfig = cfgResult.config;
|
|
460
466
|
for (const w of cfgResult.warnings) {
|
|
461
|
-
|
|
467
|
+
emit(`soly config: ${w}`, "warning");
|
|
462
468
|
}
|
|
463
469
|
if (cfgResult.sources.global || cfgResult.sources.project) {
|
|
464
470
|
const sources = [
|
|
465
471
|
cfgResult.sources.global ? `global: ${cfgResult.sources.global}` : null,
|
|
466
472
|
cfgResult.sources.project ? `project: ${cfgResult.sources.project}` : null,
|
|
467
473
|
].filter(Boolean).join(", ");
|
|
468
|
-
|
|
474
|
+
emit(`soly config loaded (${sources})`, "info");
|
|
469
475
|
}
|
|
470
476
|
// Auto-prune old iteration files per retention config
|
|
471
477
|
if (state.exists) {
|
|
472
478
|
const r = pruneOldIterations(state.solyDir, activeConfig.iteration.retentionDays);
|
|
473
479
|
if (r.pruned > 0) {
|
|
474
|
-
|
|
480
|
+
emit(
|
|
475
481
|
`soly: pruned ${r.pruned} old iteration file(s) (retention ${activeConfig.iteration.retentionDays}d)`,
|
|
476
482
|
"info",
|
|
477
483
|
);
|
|
@@ -531,9 +537,11 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
531
537
|
},
|
|
532
538
|
});
|
|
533
539
|
// Editors save in bursts (write to .tmp, rename, touch). Coalesce
|
|
534
|
-
// those rapid reload events into a single
|
|
540
|
+
// those rapid reload events into a single sub-line event under the
|
|
541
|
+
// Working indicator (└─ reloaded 47 rules). Errors here are real
|
|
542
|
+
// (disk I/O failed) and stay as popups.
|
|
535
543
|
hotReload.setNotifyHandler((reason) => {
|
|
536
|
-
|
|
544
|
+
chrome.emit(`reloaded rules (${reason})`);
|
|
537
545
|
});
|
|
538
546
|
|
|
539
547
|
// Notifications (one-shot at startup)
|
|
@@ -549,10 +557,10 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
549
557
|
if (phaseRules.length > 0) {
|
|
550
558
|
summary += ` + ${phaseRules.length} phase-${state.currentPhase?.number}`;
|
|
551
559
|
}
|
|
552
|
-
|
|
560
|
+
emit(summary, "info");
|
|
553
561
|
|
|
554
562
|
if (overriddenRulePaths.length > 0) {
|
|
555
|
-
|
|
563
|
+
emit(
|
|
556
564
|
`soly: ${overriddenRulePaths.length} rule(s) overridden by project (${overriddenRulePaths.join(", ")})`,
|
|
557
565
|
"info",
|
|
558
566
|
);
|
|
@@ -576,23 +584,23 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
576
584
|
if (added.length) parts.push(`+${added.length}`);
|
|
577
585
|
if (removed.length) parts.push(`-${removed.length}`);
|
|
578
586
|
if (changed.length) parts.push(`~${changed.length}`);
|
|
579
|
-
|
|
587
|
+
emit(`soly: rules changed since last session (${parts.join(" ")})`, "info");
|
|
580
588
|
}
|
|
581
589
|
|
|
582
590
|
// Rule budget analytics
|
|
583
591
|
const analytics = analyzeRules(alwaysOnRules, CONTEXT_WINDOW_TOKENS);
|
|
584
592
|
if (analytics.contextBudgetPct > 5) {
|
|
585
|
-
|
|
593
|
+
emit(
|
|
586
594
|
`soly: rules use ${analytics.contextBudgetPct.toFixed(1)}% of context window (${formatTok(analytics.totalTokens)} across ${analytics.fileCount} files)`,
|
|
587
595
|
"info",
|
|
588
596
|
);
|
|
589
597
|
}
|
|
590
598
|
} else {
|
|
591
|
-
|
|
599
|
+
emit("soly rules: none found in .agents/rules.local, .agents/rules, or ~/.agents/rules", "info");
|
|
592
600
|
}
|
|
593
601
|
|
|
594
602
|
if (state.exists) {
|
|
595
|
-
|
|
603
|
+
emit(`soly state: ${state.milestone} (${state.phases.length} phases)`, "info");
|
|
596
604
|
}
|
|
597
605
|
|
|
598
606
|
updateStatus(ctx);
|
package/init.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import * as fs from "node:fs";
|
|
36
36
|
import * as path from "node:path";
|
|
37
37
|
import { SOLY_DIRNAME } from "./core.js";
|
|
38
|
+
import { emit } from "./visual/event-sink.ts";
|
|
38
39
|
|
|
39
40
|
export type InitTemplate = "minimal" | "web-app" | "library" | "cli";
|
|
40
41
|
|
|
@@ -207,7 +208,7 @@ export async function initSolyProject(
|
|
|
207
208
|
|
|
208
209
|
// Preconditions
|
|
209
210
|
if (fs.existsSync(agentsDir)) {
|
|
210
|
-
|
|
211
|
+
emit(
|
|
211
212
|
`soly init: project already initialized (found ${SOLY_DIRNAME}/). ` +
|
|
212
213
|
`Aborting to avoid overwriting.`,
|
|
213
214
|
"error",
|