agentic-retro 0.1.1 → 0.1.3
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/cli/lib/commands.js +10 -2
- package/cli/lib/outcomes.js +1 -1
- package/cli/lib/pi-extension.ts +104 -5
- package/cli/lib/pi.js +7 -2
- package/cli/lib/queue.js +30 -8
- package/package.json +19 -2
- package/skills/run/SKILL.md +12 -7
package/cli/lib/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Builds the command that starts the batch agent in each harness.
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -37,7 +37,15 @@ function sessionsOf(batchDir) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function workDirs(config, sessions) {
|
|
40
|
-
const
|
|
40
|
+
const editable = (config.editable ?? []).map((path) => {
|
|
41
|
+
path = expandHome(path);
|
|
42
|
+
try {
|
|
43
|
+
return statSync(path).isFile() ? dirname(path) : path;
|
|
44
|
+
} catch {
|
|
45
|
+
return path;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
const paths = [...sessions.flatMap((s) => [s.repo, s.cwd]), ...editable];
|
|
41
49
|
return [...new Set(paths.filter(Boolean))];
|
|
42
50
|
}
|
|
43
51
|
|
package/cli/lib/outcomes.js
CHANGED
|
@@ -39,7 +39,7 @@ export function syncOutcomes() {
|
|
|
39
39
|
if (status !== 'open') setStatus(p, status);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
for (const p of open()) {
|
|
42
|
+
for (const p of open().filter((p) => !p.pr)) {
|
|
43
43
|
if (ledger.batchCount - p.batch >= expireAfterBatches) setStatus(p, 'expired');
|
|
44
44
|
}
|
|
45
45
|
saveLedger(ledger);
|
package/cli/lib/pi-extension.ts
CHANGED
|
@@ -2,27 +2,126 @@
|
|
|
2
2
|
// Queues each finished session for a retro and shows new proposals at session start.
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
|
+
import { createReadStream } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
11
|
+
const CLI = join(PACKAGE_ROOT, "cli", "agentic-retro.js");
|
|
12
|
+
const SETUP_SKILL = join(PACKAGE_ROOT, "skills", "setup");
|
|
13
|
+
const RUN_SKILL = join(PACKAGE_ROOT, "skills", "run");
|
|
14
|
+
const REVIEW_SKILL = join(PACKAGE_ROOT, "skills", "review");
|
|
15
|
+
const RETRO_SKILL = join(PACKAGE_ROOT, "skills", "retro");
|
|
16
|
+
|
|
17
|
+
type CommandContext = {
|
|
18
|
+
addUserMessage?: (message: string) => void;
|
|
19
|
+
ui?: { notify: (message: string, level?: string) => void };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function argv(args: string) {
|
|
23
|
+
const values = [];
|
|
24
|
+
let current = "";
|
|
25
|
+
let quote = "";
|
|
26
|
+
let started = false;
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
29
|
+
const char = args[i];
|
|
30
|
+
|
|
31
|
+
if (quote && char === "\\" && (args[i + 1] === quote || args[i + 1] === "\\")) {
|
|
32
|
+
current += args[i + 1];
|
|
33
|
+
started = true;
|
|
34
|
+
i += 1;
|
|
35
|
+
} else if ((char === '"' || char === "'") && (!quote || quote === char)) {
|
|
36
|
+
quote = quote ? "" : char;
|
|
37
|
+
started = true;
|
|
38
|
+
} else if (/\s/.test(char) && !quote) {
|
|
39
|
+
if (started) values.push(current);
|
|
40
|
+
current = "";
|
|
41
|
+
started = false;
|
|
42
|
+
} else {
|
|
43
|
+
current += char;
|
|
44
|
+
started = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (started) values.push(current);
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sendUserMessage(pi: ExtensionAPI, ctx: CommandContext, message: string) {
|
|
53
|
+
if (ctx.addUserMessage) return ctx.addUserMessage(message);
|
|
54
|
+
return pi.sendUserMessage(message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function skillPrompt(skill: string, args: string) {
|
|
58
|
+
const request = args.trim();
|
|
59
|
+
return `Use the agentic-retro skill at ${skill}.${request ? `\n\nUser request: ${request}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function isChildTranscript(path) {
|
|
63
|
+
try {
|
|
64
|
+
const lines = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
|
|
65
|
+
for await (const line of lines) {
|
|
66
|
+
let entry;
|
|
67
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
68
|
+
if (entry?.type !== "session") continue;
|
|
69
|
+
return Object.hasOwn(entry, "parentSession");
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
5
76
|
|
|
6
77
|
export default function (pi: ExtensionAPI) {
|
|
7
|
-
pi.
|
|
78
|
+
pi.registerCommand("agentic-retro", {
|
|
79
|
+
description: "Run the package-local agentic-retro CLI",
|
|
80
|
+
handler: async (args, ctx) => {
|
|
81
|
+
execFile(process.execPath, [CLI, ...argv(args)], (error, stdout, stderr) => {
|
|
82
|
+
const output = (stdout || stderr).trim();
|
|
83
|
+
if (output) ctx.ui.notify(output, error ? "error" : "info");
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
pi.registerCommand("agentic-retro:setup", {
|
|
89
|
+
description: "Run the bundled agentic-retro setup skill",
|
|
90
|
+
handler: async (args, ctx) => sendUserMessage(pi, ctx, skillPrompt(SETUP_SKILL, args)),
|
|
91
|
+
});
|
|
92
|
+
pi.registerCommand("agentic-retro:run", {
|
|
93
|
+
description: "Run the bundled agentic-retro run skill",
|
|
94
|
+
handler: async (args, ctx) => sendUserMessage(pi, ctx, skillPrompt(RUN_SKILL, args)),
|
|
95
|
+
});
|
|
96
|
+
pi.registerCommand("agentic-retro:review", {
|
|
97
|
+
description: "Run the bundled agentic-retro review skill",
|
|
98
|
+
handler: async (args, ctx) => sendUserMessage(pi, ctx, skillPrompt(REVIEW_SKILL, args)),
|
|
99
|
+
});
|
|
100
|
+
pi.registerCommand("agentic-retro:retro", {
|
|
101
|
+
description: "Run the bundled agentic-retro retro skill",
|
|
102
|
+
handler: async (args, ctx) => sendUserMessage(pi, ctx, skillPrompt(RETRO_SKILL, args)),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
8
106
|
if (event.reason !== "quit") return; // "new", "resume", and "fork" only switch sessions
|
|
9
107
|
if (process.env.AGENTIC_RETRO_RUN === "1") return; // batch agents never queue themselves
|
|
10
108
|
|
|
11
109
|
const transcript = ctx.sessionManager.getSessionFile();
|
|
12
110
|
if (!transcript) return; // nothing was said, so nothing was saved
|
|
111
|
+
if (await isChildTranscript(transcript)) return; // Pi sub-agent sessions are only context for their parent
|
|
13
112
|
|
|
14
|
-
const child = spawn(
|
|
15
|
-
"enqueue", "--harness", "pi",
|
|
113
|
+
const child = spawn(process.execPath, [
|
|
114
|
+
CLI, "enqueue", "--harness", "pi",
|
|
16
115
|
"--session", ctx.sessionManager.getSessionId(),
|
|
17
116
|
"--transcript", transcript,
|
|
18
117
|
"--cwd", ctx.cwd,
|
|
19
118
|
], { detached: true, stdio: "ignore" });
|
|
20
|
-
child.on("error", () => {}); // CLI
|
|
119
|
+
child.on("error", () => {}); // CLI missing/broken: never break Pi
|
|
21
120
|
child.unref();
|
|
22
121
|
});
|
|
23
122
|
|
|
24
123
|
pi.on("session_start", (_event, ctx) => {
|
|
25
|
-
execFile(
|
|
124
|
+
execFile(process.execPath, [CLI, "notice"], (error, stdout) => {
|
|
26
125
|
const line = stdout?.trim();
|
|
27
126
|
if (!error && line) ctx.ui.notify(line, "info");
|
|
28
127
|
});
|
package/cli/lib/pi.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
// Pi has no hook config, so a small extension file stands in for the hooks.
|
|
2
|
-
import { mkdirSync,
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
+
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
7
8
|
const TEMPLATE = join(dirname(fileURLToPath(import.meta.url)), 'pi-extension.ts');
|
|
8
9
|
|
|
9
10
|
export function installPi() {
|
|
10
11
|
const dir = join(process.env.PI_HOME || join(homedir(), '.pi'), 'agent', 'extensions');
|
|
11
12
|
mkdirSync(dir, { recursive: true });
|
|
12
13
|
const file = join(dir, 'agentic-retro.ts');
|
|
13
|
-
|
|
14
|
+
const source = readFileSync(TEMPLATE, 'utf8').replace(
|
|
15
|
+
'const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");',
|
|
16
|
+
`const PACKAGE_ROOT = ${JSON.stringify(PACKAGE_ROOT)};`,
|
|
17
|
+
);
|
|
18
|
+
writeFileSync(file, source);
|
|
14
19
|
console.log(`agentic-retro: wrote ${file}`);
|
|
15
20
|
}
|
package/cli/lib/queue.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
// The queue: finished sessions wait here until their group fills a batch.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { readdirSync } from 'node:fs';
|
|
3
|
+
import { createReadStream, readdirSync } from 'node:fs';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
|
-
import { join, basename } from 'node:path';
|
|
5
|
+
import { join, basename, resolve } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
6
7
|
import { readJson, writeJson } from './paths.js';
|
|
7
8
|
import { loadConfig } from './config.js';
|
|
8
9
|
import { startBatch } from './runner.js';
|
|
9
10
|
|
|
10
|
-
function repoOf(cwd) {
|
|
11
|
+
function repoOf(cwd, fallback = cwd) {
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const options = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
14
|
+
const commonDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], options).replace(/\n$/, '');
|
|
15
|
+
// Git lists the shared identity first: the main checkout or its separate metadata directory.
|
|
16
|
+
const worktrees = execFileSync('git', ['--git-dir', resolve(cwd, commonDir), 'worktree', 'list', '--porcelain', '-z'], options);
|
|
17
|
+
const main = worktrees.split('\0').find((field) => field.startsWith('worktree '));
|
|
18
|
+
return main ? main.slice('worktree '.length) : fallback;
|
|
14
19
|
} catch {
|
|
15
|
-
return
|
|
20
|
+
return fallback;
|
|
16
21
|
}
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -37,14 +42,31 @@ function harnessOf(transcript) {
|
|
|
37
42
|
return undefined;
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
async function isPiChildTranscript(transcript) {
|
|
46
|
+
if (!transcript) return false;
|
|
47
|
+
try {
|
|
48
|
+
const lines = createInterface({ input: createReadStream(transcript), crlfDelay: Infinity });
|
|
49
|
+
for await (const line of lines) {
|
|
50
|
+
let entry;
|
|
51
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
52
|
+
if (entry?.type !== 'session') continue;
|
|
53
|
+
return Object.hasOwn(entry, 'parentSession');
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
export async function enqueue({ harness, sessionId, transcript, cwd }) {
|
|
41
62
|
if (process.env.AGENTIC_RETRO_RUN === '1') return; // batch agents never queue themselves
|
|
42
63
|
transcript ||= findCodexRollout(sessionId) ?? null;
|
|
43
64
|
harness ||= harnessOf(transcript);
|
|
44
65
|
if (!harness || !sessionId || !cwd) throw new Error('enqueue needs a harness, session, and cwd');
|
|
66
|
+
if (harness === 'pi' && await isPiChildTranscript(transcript)) return;
|
|
45
67
|
|
|
46
|
-
const queue = readJson('queue.json', []);
|
|
47
|
-
if (queue.some((s) => s.sessionId === sessionId)) return;
|
|
68
|
+
const queue = readJson('queue.json', []).map((s) => ({ ...s, repo: repoOf(s.cwd, s.repo) }));
|
|
69
|
+
if (queue.some((s) => s.sessionId === sessionId)) return writeJson('queue.json', queue);
|
|
48
70
|
const entry = { harness, sessionId, transcript, cwd, repo: repoOf(cwd), endedAt: new Date().toISOString() };
|
|
49
71
|
queue.push(entry);
|
|
50
72
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-retro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "agentic-retrospective: reviews finished agent coding sessions and proposes improvements",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
6
9
|
"bin": {
|
|
7
10
|
"agentic-retro": "cli/agentic-retro.js"
|
|
8
11
|
},
|
|
@@ -13,8 +16,22 @@
|
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=20"
|
|
15
18
|
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"pi": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"cli/lib/pi-extension.ts"
|
|
30
|
+
],
|
|
31
|
+
"skills": []
|
|
32
|
+
},
|
|
16
33
|
"scripts": {
|
|
17
|
-
"test": "node --test test
|
|
34
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
18
35
|
},
|
|
19
36
|
"license": "MIT",
|
|
20
37
|
"repository": {
|
package/skills/run/SKILL.md
CHANGED
|
@@ -109,20 +109,25 @@ agentic-retro propose --repo <target repo path> --category <category> [--risk lo
|
|
|
109
109
|
|
|
110
110
|
Do this once per target repo. Skip a target with no git remote, and put its proposals in a report file instead (5b).
|
|
111
111
|
|
|
112
|
+
Before you branch, read the repo's PR rules in its `AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md` and CI workflows, such as a `dev` base branch or a version bump.
|
|
113
|
+
|
|
112
114
|
```bash
|
|
113
115
|
git -C "$repo" fetch origin
|
|
114
|
-
base=$(git -C "$repo" symbolic-ref --short refs/remotes/origin/HEAD | sed 's@^origin/@@')
|
|
116
|
+
base=$(git -C "$repo" symbolic-ref --short refs/remotes/origin/HEAD | sed 's@^origin/@@') # override with the rules' base
|
|
115
117
|
wt=$(mktemp -d)/wt
|
|
116
118
|
git -C "$repo" worktree add -b "agentic-retro/batch-<n>" "$wt" "origin/$base"
|
|
117
119
|
```
|
|
118
120
|
|
|
119
121
|
1. Apply each proposal in `$wt`, then commit it on its own: `git -C "$wt" add -A && git -C "$wt" commit -m "P12: <title>"`
|
|
120
|
-
2.
|
|
121
|
-
3.
|
|
122
|
-
4.
|
|
123
|
-
5.
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
2. Do the steps the rules require in one last commit without a proposal ID, so dropping a proposal keeps them. When a step needs a command you cannot run, say so at the top of the PR body
|
|
123
|
+
3. Push the branch: `git -C "$wt" push -u origin "agentic-retro/batch-<n>"`
|
|
124
|
+
4. Make sure the `agentic-retro` label exists from `$wt`: `(cd "$wt" && gh label create agentic-retro --force)`
|
|
125
|
+
5. Write the body per `writing-proposals.md`, then open the PR from `$wt`: `(cd "$wt" && gh pr create --base "$base" --title "<title>" --body-file <file> --label agentic-retro)`
|
|
126
|
+
6. Wait for CI with `gh pr checks <url> --watch`, then fix each failure in a new commit and push
|
|
127
|
+
7. Record the PR and commit on each proposal: `agentic-retro link P12 --pr <url> --commit <sha>`
|
|
128
|
+
8. Remove the worktree: `git -C "$repo" worktree remove "$wt"`
|
|
129
|
+
|
|
130
|
+
When `autoMergeLowRisk` is `true`, low-risk agent-setup proposals go on a second branch, `agentic-retro/batch-<n>-low-risk`, with their own PR. Merge that PR with `gh pr merge --merge` once its checks pass. Everything else waits for the user.
|
|
126
131
|
|
|
127
132
|
## 5b. Report mode: one report file
|
|
128
133
|
|