@alexgorbatchev/pi-agentation 3.0.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/LICENSE +21 -0
- package/README.md +72 -0
- package/agentation.ts +432 -0
- package/bin/pi-agentation +20 -0
- package/package.json +45 -0
- package/skills/agentation-fix-loop/SKILL.md +199 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gorbatchev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# pi plugin for Agentation Fork
|
|
2
|
+
|
|
3
|
+
A pi extension that continuously runs an Agentation fix loop by repeatedly sending:
|
|
4
|
+
|
|
5
|
+
- `/skill:agentation-fix-loop <project-id>`
|
|
6
|
+
|
|
7
|
+
It starts automatically when the session starts, resolves the project ID for the current repository (searching for `<Agentation projectId=... />`), and keeps re-queuing the same project-scoped prompt after each agent run until pi exits (or you stop it).
|
|
8
|
+
|
|
9
|
+
See:
|
|
10
|
+
|
|
11
|
+
- [Agentation Fork](https://github.com/alexgorbatchev/agentation)
|
|
12
|
+
- [CLI](https://github.com/alexgorbatchev/agentation-cli)
|
|
13
|
+
- [Agentation Skills](https://github.com/alexgorbatchev/agentation-skills)
|
|
14
|
+
|
|
15
|
+
> [!IMPORTANT]
|
|
16
|
+
> This loops AI until manually stopped and so it can consume tokens while idling. Don't forget to stop it when you no longer using it.
|
|
17
|
+
|
|
18
|
+
## Behavior
|
|
19
|
+
|
|
20
|
+
- The launcher (`pi-agentation`) injects the local packaged fix-loop skill via `--skill`
|
|
21
|
+
- Extension checks that `/skill:agentation-fix-loop` is available before running
|
|
22
|
+
- On session start/switch/fork, the extension:
|
|
23
|
+
- runs `agentation projects --json`
|
|
24
|
+
- runs `rg` to discover literal `projectId="..."` or `projectId='...'` values in the repo
|
|
25
|
+
- intersects both lists
|
|
26
|
+
- auto-starts if exactly one project matches, otherwise prompts you to choose in the TUI
|
|
27
|
+
- The resolved project ID is stored in the current Pi session so reloads/resume do not re-prompt that same session
|
|
28
|
+
- On `agent_end`: sends the next project-scoped loop prompt
|
|
29
|
+
- On `session_shutdown`: stops loop automatically
|
|
30
|
+
- If the skill is missing, no repo project IDs are found, or no discovered repo IDs are known to Agentation yet: plugin requests shutdown and exits with code `1`
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
- `/agentation-loop-start` — resume/start looping
|
|
35
|
+
- `/agentation-loop-stop` — pause looping
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
Install both project packages:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -D @alexgorbatchev/agentation @alexgorbatchev/pi-agentation
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`@alexgorbatchev/pi-agentation` ships its own packaged copy of the fix-loop skill. That file is synced from [`@alexgorbatchev/agentation-skills`](https://github.com/alexgorbatchev/agentation-skills) during packaging, so you do not need to install the skill package separately.
|
|
46
|
+
|
|
47
|
+
Required executables on `PATH`:
|
|
48
|
+
|
|
49
|
+
- `pi`
|
|
50
|
+
- `agentation`
|
|
51
|
+
- `rg`
|
|
52
|
+
|
|
53
|
+
The `agentation` [CLI](https://github.com/alexgorbatchev/agentation-cli) is distributed separately from these npm packages and must be downloaded and placed on your `PATH`.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Before running the pi, you need to start the [CLI](https://github.com/alexgorbatchev/agentation-cli) and connect from the front end which has `<Agentation projectId="..." />` at least once in the last 24h. Then run the launcher from your project:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx pi-agentation
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If your shell already exposes local package binaries on `PATH`, you can run:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pi-agentation
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- This loop is intentionally persistent and can consume tokens quickly.
|
|
72
|
+
- Use `/agentation-loop-stop` if you want to pause it without exiting Pi.
|
package/agentation.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
const LOOP_SKILL_NAME = "agentation-fix-loop";
|
|
5
|
+
const LOOP_PROMPT = `/skill:${LOOP_SKILL_NAME}`;
|
|
6
|
+
const PROJECT_SELECTION_ENTRY_TYPE = "agentation-project-selection";
|
|
7
|
+
const PROJECT_ID_PATTERN = /^projectId=(?:"([^"\r\n]+)"|'([^'\r\n]+)')$/;
|
|
8
|
+
|
|
9
|
+
type ExecResult = Awaited<ReturnType<ExtensionAPI["exec"]>>;
|
|
10
|
+
|
|
11
|
+
type CommandOutcome = {
|
|
12
|
+
code: number | undefined;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
killed: boolean;
|
|
15
|
+
stderr: string;
|
|
16
|
+
stdout: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface IProjectSelectionData {
|
|
20
|
+
projectId: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function agentation(pi: ExtensionAPI): void {
|
|
24
|
+
let currentProjectId: string | null = null;
|
|
25
|
+
let isLoopEnabled = true;
|
|
26
|
+
|
|
27
|
+
const isLoopSkillAvailable = (): boolean => {
|
|
28
|
+
return pi.getCommands().some((command) => {
|
|
29
|
+
if (command.source !== "skill") {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return command.name === LOOP_SKILL_NAME || command.name === `skill:${LOOP_SKILL_NAME}`;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const reportError = (ctx: ExtensionContext, message: string): void => {
|
|
38
|
+
console.error(message);
|
|
39
|
+
ctx.ui.notify(message, "error");
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const shutdownForFatalError = (ctx: ExtensionContext, message: string): void => {
|
|
43
|
+
currentProjectId = null;
|
|
44
|
+
isLoopEnabled = false;
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
reportError(ctx, message);
|
|
47
|
+
ctx.shutdown();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ensureLoopSkillAvailable = (ctx: ExtensionContext): boolean => {
|
|
51
|
+
if (isLoopSkillAvailable()) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
shutdownForFatalError(ctx, `Missing required skill ${LOOP_PROMPT}. Exiting.`);
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const queueLoopPrompt = (projectId: string, deliverAsFollowUp: boolean): void => {
|
|
60
|
+
if (!isLoopEnabled) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const loopPrompt = `${LOOP_PROMPT} ${projectId}`;
|
|
65
|
+
if (deliverAsFollowUp) {
|
|
66
|
+
pi.sendUserMessage(loopPrompt, { deliverAs: "followUp" });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pi.sendUserMessage(loopPrompt);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const persistProjectSelection = (projectId: string): void => {
|
|
74
|
+
if (currentProjectId === projectId) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
currentProjectId = projectId;
|
|
79
|
+
pi.appendEntry(PROJECT_SELECTION_ENTRY_TYPE, { projectId });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const restoreProjectSelection = (ctx: ExtensionContext): string | null => {
|
|
83
|
+
let latestProjectId: string | null = null;
|
|
84
|
+
let latestTimestamp = "";
|
|
85
|
+
|
|
86
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
87
|
+
if (entry.type !== "custom" || entry.customType !== PROJECT_SELECTION_ENTRY_TYPE) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!isProjectSelectionData(entry.data)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const normalizedProjectId = normalizeProjectId(entry.data.projectId);
|
|
96
|
+
if (normalizedProjectId === null) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (entry.timestamp >= latestTimestamp) {
|
|
101
|
+
latestTimestamp = entry.timestamp;
|
|
102
|
+
latestProjectId = normalizedProjectId;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
currentProjectId = latestProjectId;
|
|
107
|
+
return latestProjectId;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const execCommand = async (command: string, args: string[]): Promise<CommandOutcome> => {
|
|
111
|
+
try {
|
|
112
|
+
const result: ExecResult = await pi.exec(command, args);
|
|
113
|
+
return {
|
|
114
|
+
code: result.code,
|
|
115
|
+
killed: result.killed,
|
|
116
|
+
stderr: result.stderr,
|
|
117
|
+
stdout: result.stdout,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return {
|
|
121
|
+
code: undefined,
|
|
122
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
123
|
+
killed: false,
|
|
124
|
+
stderr: "",
|
|
125
|
+
stdout: "",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const listKnownProjectIds = async (): Promise<{ errorMessage?: string; projectIds: string[] }> => {
|
|
131
|
+
let projectsResult = await execCommand("agentation", ["projects", "--json"]);
|
|
132
|
+
if (!didCommandSucceed(projectsResult)) {
|
|
133
|
+
await execCommand("agentation", ["start", "--background"]);
|
|
134
|
+
projectsResult = await execCommand("agentation", ["projects", "--json"]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!didCommandSucceed(projectsResult)) {
|
|
138
|
+
const failureMessage = formatCommandOutcome(projectsResult);
|
|
139
|
+
return {
|
|
140
|
+
errorMessage: `Failed to load Agentation projects via \`agentation projects --json\`: ${failureMessage}`,
|
|
141
|
+
projectIds: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const projectIds = parseJsonStringArray(projectsResult.stdout);
|
|
146
|
+
if (projectIds === null) {
|
|
147
|
+
return {
|
|
148
|
+
errorMessage: "`agentation projects --json` returned invalid JSON.",
|
|
149
|
+
projectIds: [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { projectIds };
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const discoverRepoProjectIds = async (): Promise<{ errorMessage?: string; projectIds: string[] }> => {
|
|
157
|
+
const rgResult = await execCommand("rg", [
|
|
158
|
+
"-o",
|
|
159
|
+
"--no-filename",
|
|
160
|
+
"--glob",
|
|
161
|
+
"*.{tsx,jsx}",
|
|
162
|
+
"projectId=(?:\"[^\"]+\"|'[^']+')",
|
|
163
|
+
".",
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
if (didCommandSucceed(rgResult)) {
|
|
167
|
+
return { projectIds: extractProjectIdsFromRgOutput(rgResult.stdout) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (rgResult.code === 1 && !rgResult.killed && rgResult.errorMessage === undefined) {
|
|
171
|
+
return { projectIds: [] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
errorMessage: `Failed to discover project IDs via \`rg\`: ${formatCommandOutcome(rgResult)}`,
|
|
176
|
+
projectIds: [],
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const resolveProjectId = async (ctx: ExtensionContext): Promise<string | null> => {
|
|
181
|
+
const knownProjectsResult = await listKnownProjectIds();
|
|
182
|
+
if (knownProjectsResult.errorMessage !== undefined) {
|
|
183
|
+
shutdownForFatalError(ctx, knownProjectsResult.errorMessage);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const repoProjectsResult = await discoverRepoProjectIds();
|
|
188
|
+
if (repoProjectsResult.errorMessage !== undefined) {
|
|
189
|
+
shutdownForFatalError(ctx, repoProjectsResult.errorMessage);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (repoProjectsResult.projectIds.length === 0) {
|
|
194
|
+
const missingProjectIdMessage = [
|
|
195
|
+
"No literal Agentation `projectId` values were found in this repository.",
|
|
196
|
+
"Agentation requires an approved literal `projectId` pattern.",
|
|
197
|
+
].join(" ");
|
|
198
|
+
shutdownForFatalError(ctx, missingProjectIdMessage);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const candidateProjectIds = intersectProjectIds(repoProjectsResult.projectIds, knownProjectsResult.projectIds);
|
|
203
|
+
if (candidateProjectIds.length === 0) {
|
|
204
|
+
const repoProjectIds = repoProjectsResult.projectIds.join(", ");
|
|
205
|
+
const unknownProjectMessage = [
|
|
206
|
+
`Found repository project IDs (${repoProjectIds}), but none are known to Agentation yet.`,
|
|
207
|
+
"Open the UI so it connects to the server at least once, then retry.",
|
|
208
|
+
].join(" ");
|
|
209
|
+
shutdownForFatalError(ctx, unknownProjectMessage);
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (candidateProjectIds.length === 1) {
|
|
214
|
+
return candidateProjectIds[0] ?? null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!ctx.hasUI) {
|
|
218
|
+
const candidateProjectLabel = candidateProjectIds.join(", ");
|
|
219
|
+
const selectionMessage = [
|
|
220
|
+
`Multiple Agentation projects matched this repository (${candidateProjectLabel}),`,
|
|
221
|
+
"but no interactive UI is available to choose one.",
|
|
222
|
+
].join(" ");
|
|
223
|
+
shutdownForFatalError(ctx, selectionMessage);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const selectedProjectId = await ctx.ui.select("Select Agentation project", candidateProjectIds);
|
|
228
|
+
if (selectedProjectId === undefined) {
|
|
229
|
+
shutdownForFatalError(ctx, "Agentation project selection was cancelled.");
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return selectedProjectId;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const initializeLoopForSession = async (ctx: ExtensionContext): Promise<void> => {
|
|
237
|
+
restoreProjectSelection(ctx);
|
|
238
|
+
|
|
239
|
+
if (!isLoopEnabled) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!ensureLoopSkillAvailable(ctx)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let projectId = currentProjectId;
|
|
248
|
+
if (projectId === null) {
|
|
249
|
+
projectId = await resolveProjectId(ctx);
|
|
250
|
+
if (projectId === null) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
persistProjectSelection(projectId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
ctx.ui.notify(`Agentation loop started for ${projectId}`, "info");
|
|
257
|
+
queueLoopPrompt(projectId, !ctx.isIdle());
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
261
|
+
await initializeLoopForSession(ctx);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
265
|
+
await initializeLoopForSession(ctx);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
269
|
+
await initializeLoopForSession(ctx);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
273
|
+
if (!isLoopEnabled) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!ensureLoopSkillAvailable(ctx)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (currentProjectId === null) {
|
|
282
|
+
reportError(ctx, "Agentation loop is enabled, but no project ID is selected for this session.");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
queueLoopPrompt(currentProjectId, !ctx.isIdle());
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
pi.on("session_shutdown", async () => {
|
|
290
|
+
currentProjectId = null;
|
|
291
|
+
isLoopEnabled = false;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
pi.registerCommand("agentation-loop-start", {
|
|
295
|
+
description: "Start the automatic Agentation fix loop",
|
|
296
|
+
handler: async (_args, ctx) => {
|
|
297
|
+
if (isLoopEnabled) {
|
|
298
|
+
ctx.ui.notify("Agentation loop is already running", "info");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!ensureLoopSkillAvailable(ctx)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let projectId = currentProjectId ?? restoreProjectSelection(ctx);
|
|
307
|
+
if (projectId === null) {
|
|
308
|
+
projectId = await resolveProjectId(ctx);
|
|
309
|
+
if (projectId === null) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
persistProjectSelection(projectId);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
isLoopEnabled = true;
|
|
316
|
+
ctx.ui.notify(`Agentation loop resumed for ${projectId}`, "info");
|
|
317
|
+
queueLoopPrompt(projectId, !ctx.isIdle());
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
pi.registerCommand("agentation-loop-stop", {
|
|
322
|
+
description: "Stop the automatic Agentation fix loop",
|
|
323
|
+
handler: async (_args, ctx) => {
|
|
324
|
+
isLoopEnabled = false;
|
|
325
|
+
ctx.ui.notify("Agentation loop paused", "warning");
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function didCommandSucceed(commandOutcome: CommandOutcome): boolean {
|
|
331
|
+
return commandOutcome.code === 0 && !commandOutcome.killed && commandOutcome.errorMessage === undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function formatCommandOutcome(commandOutcome: CommandOutcome): string {
|
|
335
|
+
if (commandOutcome.errorMessage !== undefined) {
|
|
336
|
+
return commandOutcome.errorMessage;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const stderr = commandOutcome.stderr.trim();
|
|
340
|
+
if (stderr !== "") {
|
|
341
|
+
return stderr;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const stdout = commandOutcome.stdout.trim();
|
|
345
|
+
if (stdout !== "") {
|
|
346
|
+
return stdout;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (commandOutcome.killed) {
|
|
350
|
+
return "command was killed";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (commandOutcome.code !== undefined) {
|
|
354
|
+
return `exit code ${commandOutcome.code}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return "unknown failure";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeProjectId(projectId: string): string | null {
|
|
361
|
+
const trimmedProjectId = projectId.trim();
|
|
362
|
+
if (trimmedProjectId === "") {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return trimmedProjectId;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeProjectIds(projectIds: readonly string[]): string[] {
|
|
370
|
+
const normalizedProjectIds = new Set<string>();
|
|
371
|
+
for (const projectId of projectIds) {
|
|
372
|
+
const normalizedProjectId = normalizeProjectId(projectId);
|
|
373
|
+
if (normalizedProjectId !== null) {
|
|
374
|
+
normalizedProjectIds.add(normalizedProjectId);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return Array.from(normalizedProjectIds).sort((leftProjectId, rightProjectId) => {
|
|
379
|
+
return leftProjectId.localeCompare(rightProjectId);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function intersectProjectIds(repoProjectIds: readonly string[], knownProjectIds: readonly string[]): string[] {
|
|
384
|
+
const knownProjectIdSet = new Set(normalizeProjectIds(knownProjectIds));
|
|
385
|
+
return normalizeProjectIds(repoProjectIds).filter((projectId) => {
|
|
386
|
+
return knownProjectIdSet.has(projectId);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function extractProjectIdsFromRgOutput(output: string): string[] {
|
|
391
|
+
const projectIds: string[] = [];
|
|
392
|
+
for (const line of output.split(/\r?\n/)) {
|
|
393
|
+
const trimmedLine = line.trim();
|
|
394
|
+
if (trimmedLine === "") {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const match = PROJECT_ID_PATTERN.exec(trimmedLine);
|
|
399
|
+
const extractedProjectId = match?.[1] ?? match?.[2];
|
|
400
|
+
if (extractedProjectId !== undefined) {
|
|
401
|
+
projectIds.push(extractedProjectId);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return normalizeProjectIds(projectIds);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
409
|
+
return typeof value === "object" && value !== null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isStringArray(value: unknown): value is string[] {
|
|
413
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isProjectSelectionData(value: unknown): value is IProjectSelectionData {
|
|
417
|
+
if (!isRecord(value)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const projectId = value["projectId"];
|
|
422
|
+
return typeof projectId === "string" && projectId.trim() !== "";
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function parseJsonStringArray(jsonText: string): string[] | null {
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(jsonText);
|
|
428
|
+
return isStringArray(parsed) ? normalizeProjectIds(parsed) : null;
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SOURCE_PATH="${BASH_SOURCE[0]}"
|
|
5
|
+
while [[ -L "${SOURCE_PATH}" ]]; do
|
|
6
|
+
SOURCE_DIRECTORY="$(cd -P "$(dirname "${SOURCE_PATH}")" && pwd)"
|
|
7
|
+
SOURCE_PATH="$(readlink "${SOURCE_PATH}")"
|
|
8
|
+
|
|
9
|
+
if [[ "${SOURCE_PATH}" != /* ]]; then
|
|
10
|
+
SOURCE_PATH="${SOURCE_DIRECTORY}/${SOURCE_PATH}"
|
|
11
|
+
fi
|
|
12
|
+
done
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd -P "$(dirname "${SOURCE_PATH}")" && pwd)"
|
|
15
|
+
PACKAGE_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
16
|
+
|
|
17
|
+
exec pi \
|
|
18
|
+
-e "${PACKAGE_ROOT}/agentation.ts" \
|
|
19
|
+
--skill "${PACKAGE_ROOT}/skills/agentation-fix-loop/SKILL.md" \
|
|
20
|
+
"$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alexgorbatchev/pi-agentation",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "pi extension launcher for the Agentation fix loop",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"agentation"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"pi-agentation": "./bin/pi-agentation"
|
|
13
|
+
},
|
|
14
|
+
"pi": {
|
|
15
|
+
"extensions": [
|
|
16
|
+
"./agentation.ts"
|
|
17
|
+
],
|
|
18
|
+
"skills": [
|
|
19
|
+
"./skills"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"agentation.ts",
|
|
24
|
+
"README.md",
|
|
25
|
+
"bin/pi-agentation",
|
|
26
|
+
"skills/agentation-fix-loop/SKILL.md"
|
|
27
|
+
],
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@alexgorbatchev/agentation-skills": "^1.0.0",
|
|
33
|
+
"@mariozechner/pi-coding-agent": "^0.61.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"check": "npm run sync-skill && npm run verify:package && npm run verify:launcher",
|
|
37
|
+
"prepack": "npm run sync-skill",
|
|
38
|
+
"sync-skill": "node ./scripts/syncBundledSkill.js",
|
|
39
|
+
"verify:package": "npm pack --dry-run > /dev/null",
|
|
40
|
+
"verify:launcher": "PI_OFFLINE=1 ./bin/pi-agentation --list-models > /dev/null"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agentation-fix-loop
|
|
3
|
+
description: >-
|
|
4
|
+
Watch for Agentation annotations and fix each one using the Agentation CLI.
|
|
5
|
+
Runs `agentation watch` in a loop — acknowledges each annotation, makes the
|
|
6
|
+
code fix, then resolves it. Use when the user says "watch annotations",
|
|
7
|
+
"fix annotations", "annotation loop", "agentation fix loop", or wants
|
|
8
|
+
autonomous processing of design feedback from the Agentation toolbar.
|
|
9
|
+
targets:
|
|
10
|
+
- '*'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Agentation Fix Loop (CLI)
|
|
14
|
+
|
|
15
|
+
Watch for annotations from the Agentation toolbar and fix each one in the codebase using the `agentation` CLI.
|
|
16
|
+
|
|
17
|
+
If this skill is invoked as `/skill:agentation-fix-loop <project-id>`, treat the user-supplied argument as the authoritative project ID for this run and skip repo discovery.
|
|
18
|
+
|
|
19
|
+
## CLI commands used by this skill
|
|
20
|
+
|
|
21
|
+
- `agentation start` / `agentation stop` / `agentation status`
|
|
22
|
+
- `agentation projects --json`
|
|
23
|
+
- `agentation pending <project-id> --json`
|
|
24
|
+
- `agentation watch <project-id> --json`
|
|
25
|
+
- `agentation ack <id>`
|
|
26
|
+
- `agentation resolve <id> --summary "..."`
|
|
27
|
+
- `agentation reply <id> --message "..."`
|
|
28
|
+
- `agentation dismiss <id> --reason "..."`
|
|
29
|
+
|
|
30
|
+
## Preflight (required)
|
|
31
|
+
|
|
32
|
+
### 1) Ensure the Agentation CLI is available
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
command -v agentation >/dev/null || { echo "agentation CLI not found on PATH"; exit 1; }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If this fails, install or build the Agentation CLI first and ensure the `agentation` binary is on `PATH`.
|
|
39
|
+
|
|
40
|
+
### 2) Check whether the Agentation stack is already running
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
agentation status
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then verify API reachability (default `http://127.0.0.1:4747`):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
agentation projects --json >/dev/null
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If not running or unreachable, **start it before doing anything else**:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agentation start --background
|
|
56
|
+
# or foreground during debugging
|
|
57
|
+
agentation start --foreground
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Re-check after start:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
agentation status
|
|
64
|
+
agentation projects --json >/dev/null
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you only want the HTTP API without router for this run:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
AGENTATION_ROUTER_ADDR=0 agentation start --background
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 3) Determine the project ID and fetch pending work
|
|
74
|
+
|
|
75
|
+
If the skill was invoked with a user argument (for example `/skill:agentation-fix-loop project-alpha`), use that `<project-id>` directly and skip discovery.
|
|
76
|
+
|
|
77
|
+
Otherwise, quickly extract project IDs from your app code:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
rg -n --glob '*.{tsx,ts,jsx,js}' '<Agentation[^>]*projectId='
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If you want to extract a literal string value quickly (when set as `projectId="..."` or `projectId='...'`):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
rg -o --no-filename --glob '*.{tsx,ts,jsx,js}' "projectId=(?:\"[^\"]+\"|'[^']+')" \
|
|
87
|
+
| head -n1 \
|
|
88
|
+
| sed -E "s/projectId=(\"([^\"]+)\"|'([^']+)')/\2\3/"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then fetch the initial batch:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
agentation pending <project-id> --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Process that batch first, then enter watch mode.
|
|
98
|
+
|
|
99
|
+
### CLI behavior notes
|
|
100
|
+
|
|
101
|
+
- `agentation start` manages server + router under one process by default.
|
|
102
|
+
- One running Agentation stack is enough for multiple local projects/sessions.
|
|
103
|
+
- Do not start extra instances unless intentionally isolating ports/storage.
|
|
104
|
+
|
|
105
|
+
## Behavior
|
|
106
|
+
|
|
107
|
+
1. Call:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
agentation watch <project-id> --timeout 300 --batch-window 10 --json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
2. For each annotation in the returned batch:
|
|
114
|
+
|
|
115
|
+
a. **Acknowledge**
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
agentation ack <annotation-id>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
b. **Understand**
|
|
122
|
+
- Read annotation text (`comment`)
|
|
123
|
+
- Read target context (`element`, `elementPath`, `url`, `nearbyText`, `reactComponents`)
|
|
124
|
+
- Map to likely source files before editing
|
|
125
|
+
|
|
126
|
+
c. **Fix**
|
|
127
|
+
- Make the code change requested by the annotation
|
|
128
|
+
- Keep changes minimal and aligned with project conventions
|
|
129
|
+
|
|
130
|
+
d. **Resolve**
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
agentation resolve <annotation-id> --summary "<short file + change summary>"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
3. After processing the batch, loop back to step 1.
|
|
137
|
+
|
|
138
|
+
4. Stop when:
|
|
139
|
+
- user says stop, or
|
|
140
|
+
- watch times out repeatedly with no new work.
|
|
141
|
+
|
|
142
|
+
## Rules
|
|
143
|
+
|
|
144
|
+
- Always acknowledge before starting work.
|
|
145
|
+
- Keep resolve summaries concise (1–2 sentences, mention file(s) + result).
|
|
146
|
+
- If unclear, ask via thread reply instead of guessing:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
agentation reply <annotation-id> --message "I need clarification on ..."
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- If not actionable, dismiss with reason:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
agentation dismiss <annotation-id> --reason "Not actionable because ..."
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- Process annotations in received order.
|
|
159
|
+
- Only resolve once the requested change is implemented.
|
|
160
|
+
|
|
161
|
+
## Required project-scoped loop
|
|
162
|
+
|
|
163
|
+
Use `<project-id>` as the first argument for all pending/watch commands. Use timeout of at least 300s:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
agentation projects --json
|
|
167
|
+
agentation pending <project-id> --json
|
|
168
|
+
agentation watch <project-id> --timeout 300 --batch-window 10 --json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Loop template
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
Round 1:
|
|
175
|
+
agentation pending <project-id> --json
|
|
176
|
+
-> process all returned annotations
|
|
177
|
+
|
|
178
|
+
Round 2:
|
|
179
|
+
agentation watch <project-id> --timeout 300 --batch-window 10 --json
|
|
180
|
+
-> got 2 annotations
|
|
181
|
+
-> ack #1, fix, resolve #1
|
|
182
|
+
-> ack #2, reply (needs clarification)
|
|
183
|
+
|
|
184
|
+
Round 3:
|
|
185
|
+
agentation watch <project-id> --timeout 300 --batch-window 10 --json
|
|
186
|
+
-> got 1 annotation (clarification follow-up)
|
|
187
|
+
-> ack, fix, resolve
|
|
188
|
+
|
|
189
|
+
Round 4:
|
|
190
|
+
agentation watch <project-id> --timeout 300 --batch-window 10 --json
|
|
191
|
+
-> timeout true, no annotations
|
|
192
|
+
-> exit (or continue if user requested persistent watch mode)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Troubleshooting
|
|
196
|
+
|
|
197
|
+
- `agentation pending` fails: Agentation is not running, base URL is wrong (`agentation start --background`), or `<project-id>` is missing.
|
|
198
|
+
- If using non-default server URL, pass `--base-url` or set `AGENTATION_BASE_URL`.
|
|
199
|
+
- If frontend keeps creating new sessions unexpectedly, verify localStorage/session behavior in the host app or Storybook setup.
|