agentic-retro 0.1.2 → 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.
@@ -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.on("session_shutdown", (event, ctx) => {
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("agentic-retro", [
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 not installed: never break Pi
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("agentic-retro", ["notice"], (error, stdout) => {
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, copyFileSync } from 'node:fs';
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
- copyFileSync(TEMPLATE, file);
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,8 +1,9 @@
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
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';
@@ -41,11 +42,28 @@ function harnessOf(transcript) {
41
42
  return undefined;
42
43
  }
43
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
+
44
61
  export async function enqueue({ harness, sessionId, transcript, cwd }) {
45
62
  if (process.env.AGENTIC_RETRO_RUN === '1') return; // batch agents never queue themselves
46
63
  transcript ||= findCodexRollout(sessionId) ?? null;
47
64
  harness ||= harnessOf(transcript);
48
65
  if (!harness || !sessionId || !cwd) throw new Error('enqueue needs a harness, session, and cwd');
66
+ if (harness === 'pi' && await isPiChildTranscript(transcript)) return;
49
67
 
50
68
  const queue = readJson('queue.json', []).map((s) => ({ ...s, repo: repoOf(s.cwd, s.repo) }));
51
69
  if (queue.some((s) => s.sessionId === sessionId)) return writeJson('queue.json', queue);
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "agentic-retro",
3
- "version": "0.1.2",
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/*.test.js"
34
+ "test": "node --test \"test/**/*.test.js\""
18
35
  },
19
36
  "license": "MIT",
20
37
  "repository": {