agentic-retro 0.1.2 → 0.1.4
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 +27 -5
- package/cli/agentic-retro.js +5 -2
- package/cli/lib/notice.js +8 -5
- package/cli/lib/pi-extension.ts +104 -5
- package/cli/lib/pi.js +7 -2
- package/cli/lib/project.js +42 -0
- package/cli/lib/queue.js +23 -17
- package/cli/lib/setup.js +1 -1
- package/package.json +19 -2
- package/skills/setup/SKILL.md +9 -1
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@ agentic-retro ("agentic-retrospective") learns from your finished coding session
|
|
|
6
6
|
|
|
7
7
|
```mermaid
|
|
8
8
|
flowchart TD
|
|
9
|
-
A["Session ends<br/>(hook in each harness)"] -->
|
|
9
|
+
A["Session ends<br/>(hook in each harness)"] --> P{"Project enabled?"}
|
|
10
|
+
P -->|no| Z["Stop"]
|
|
11
|
+
P -->|yes| B["CLI adds session to queue"]
|
|
10
12
|
B --> C{"5 sessions queued for this repo?"}
|
|
11
|
-
C -->|no| Z
|
|
13
|
+
C -->|no| Z
|
|
12
14
|
C -->|yes| D["CLI writes digests and metrics,<br/>and updates past proposal outcomes"]
|
|
13
15
|
D --> E["Runner starts one lead agent;<br/>it runs one subagent per ticked category<br/>(Claude, Codex, Pi, or herdr pane)"]
|
|
14
16
|
E --> F["Subagents write proposals,<br/>skipping anything rejected or expired before"]
|
|
@@ -17,7 +19,7 @@ flowchart TD
|
|
|
17
19
|
H --> I["User reviews proposals"]
|
|
18
20
|
```
|
|
19
21
|
|
|
20
|
-
- A session-end hook
|
|
22
|
+
- A session-end hook queues sessions only for explicitly enabled repositories (`agentic-retro enqueue`)
|
|
21
23
|
- Once a repo has 5 queued sessions (configurable), the CLI starts a batch
|
|
22
24
|
- One lead agent runs per batch, and it runs one subagent per ticked category: code clarity, agent setup, repo changes
|
|
23
25
|
- Agents write proposals as a PR (one per repo) or a report file, per your setup choice
|
|
@@ -73,6 +75,23 @@ Both write the same `~/.agentic-retro/config.json`:
|
|
|
73
75
|
- which categories run, and the output mode (PR or report)
|
|
74
76
|
- where agent-setup proposals land
|
|
75
77
|
|
|
78
|
+
Enable collection separately in each repository you want reviewed:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
agentic-retro project enable
|
|
82
|
+
agentic-retro project status
|
|
83
|
+
# Stop collection and startup notices for this repository:
|
|
84
|
+
agentic-retro project disable
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Every repository starts disabled, including repositories used before this update
|
|
88
|
+
- Run these commands from any worktree or subdirectory, or pass `--cwd <dir>`
|
|
89
|
+
- All worktrees share the setting, including worktrees elsewhere on the machine
|
|
90
|
+
- Git stores `agentic-retro.enabled` in its shared local configuration, outside tracked files
|
|
91
|
+
- Separate clones require separate opt-in
|
|
92
|
+
- Disabling retains queued sessions but excludes them from future batches until re-enabled
|
|
93
|
+
- Disabling does not stop an already-running batch
|
|
94
|
+
|
|
76
95
|
Using Pi? Run `agentic-retro install-pi` once. Pi has no hook config, so this extension queues finished Pi sessions and shows the notice.
|
|
77
96
|
|
|
78
97
|
Using Codex? It asks you to trust the new session-end hook the first time a session ends after install.
|
|
@@ -88,7 +107,8 @@ Using Codex? It asks you to trust the new session-end hook the first time a sess
|
|
|
88
107
|
|
|
89
108
|
- `agentic-retro about` – print a short intro to what agentic-retro does
|
|
90
109
|
- `agentic-retro setup` – interactive config, the human alternative to the setup skill
|
|
91
|
-
- `agentic-retro config list | get <key> | set <key> <value>` – read or write one config key
|
|
110
|
+
- `agentic-retro config list | get <key> | set <key> <value>` – read or write one global config key
|
|
111
|
+
- `agentic-retro project enable | disable | status [--cwd <dir>]` – control collection and notices for one repository
|
|
92
112
|
- `agentic-retro enqueue` – add a finished session to the queue (hooks call this)
|
|
93
113
|
- `agentic-retro propose` – record a new proposal (batch agents call this)
|
|
94
114
|
- `agentic-retro link <id>` – attach a PR and commit to a proposal
|
|
@@ -101,4 +121,6 @@ Using Codex? It asks you to trust the new session-end hook the first time a sess
|
|
|
101
121
|
|
|
102
122
|
## State
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
- Runtime state lives under `~/.agentic-retro/` (override with `AGENTIC_RETRO_HOME`): config, queue, ledger, metrics, batch digests, and reports
|
|
125
|
+
- Repository activation lives in Git's shared local configuration and never enters the tracked checkout
|
|
126
|
+
- Keep runtime state outside repositories; Git ignores its own metadata automatically
|
package/cli/agentic-retro.js
CHANGED
|
@@ -11,17 +11,19 @@ import { notice } from './lib/notice.js';
|
|
|
11
11
|
import { stats } from './lib/stats.js';
|
|
12
12
|
import { installPi } from './lib/pi.js';
|
|
13
13
|
import { INTRO } from './lib/intro.js';
|
|
14
|
+
import { project } from './lib/project.js';
|
|
14
15
|
|
|
15
16
|
const USAGE = `usage: agentic-retro <command>
|
|
16
17
|
about
|
|
17
18
|
setup
|
|
19
|
+
project enable | disable | status [--cwd <dir>]
|
|
18
20
|
config list | get <key> | set <key> <value>
|
|
19
21
|
enqueue [--harness <h>] [--session <id> --transcript <path> --cwd <dir>] (or hook JSON on stdin)
|
|
20
22
|
propose --repo <path> --category <c> [--risk low|high] --title <t> [--pr <pr> --commit <msg>]
|
|
21
23
|
mark <id> <accepted|rejected>
|
|
22
24
|
link <id> [--pr <url>] [--commit <sha>]
|
|
23
25
|
ledger [--status <s>[,<s>...]]
|
|
24
|
-
notice [--json]
|
|
26
|
+
notice [--json] [--cwd <dir>]
|
|
25
27
|
stats [--json]
|
|
26
28
|
sync
|
|
27
29
|
install-pi`;
|
|
@@ -69,13 +71,14 @@ async function main() {
|
|
|
69
71
|
|
|
70
72
|
if (command === 'setup') return setup();
|
|
71
73
|
if (command === 'config') return config(positionals);
|
|
74
|
+
if (command === 'project') return project(positionals[0], flags.cwd);
|
|
72
75
|
if (command === 'enqueue') return enqueueCommand(flags);
|
|
73
76
|
if (command === 'propose') return console.log(propose(flags));
|
|
74
77
|
if (command === 'mark') return mark(...positionals);
|
|
75
78
|
if (command === 'link') return link(positionals[0], flags);
|
|
76
79
|
if (command === 'ledger') return listProposals(flags.status).forEach((p) => console.log(formatProposal(p)));
|
|
77
80
|
if (command === 'sync') return syncOutcomes();
|
|
78
|
-
if (command === 'notice') return notice(flags);
|
|
81
|
+
if (command === 'notice') return notice({ ...flags, cwd: flags.cwd ?? readHook().cwd ?? process.cwd() });
|
|
79
82
|
if (command === 'stats') return stats(flags);
|
|
80
83
|
if (command === 'install-pi') return installPi();
|
|
81
84
|
if (command === 'about') return process.stdout.write(INTRO);
|
package/cli/lib/notice.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { stateDir, statePath, readJson, writeJson } from './paths.js';
|
|
4
4
|
import { loadConfig } from './config.js';
|
|
5
5
|
import { listProposals } from './ledger.js';
|
|
6
|
+
import { projectEnabled, repoOf } from './project.js';
|
|
6
7
|
|
|
7
8
|
function linkFor(proposal, output) {
|
|
8
9
|
return output === 'report' ? statePath('reports', `${proposal.batch}.md`) : proposal.pr;
|
|
@@ -10,13 +11,15 @@ function linkFor(proposal, output) {
|
|
|
10
11
|
|
|
11
12
|
// notice.json remembers which open proposals were already shown with their link.
|
|
12
13
|
// In PR mode a proposal without a PR stays new, so a later notice can name its PR.
|
|
13
|
-
export function noticeLine() {
|
|
14
|
-
if (process.env.AGENTIC_RETRO_RUN === '1' || !existsSync(stateDir())) return '';
|
|
14
|
+
export function noticeLine(cwd = process.cwd()) {
|
|
15
|
+
if (process.env.AGENTIC_RETRO_RUN === '1' || !projectEnabled(cwd) || !existsSync(stateDir())) return '';
|
|
16
|
+
const repo = repoOf(cwd);
|
|
15
17
|
|
|
16
18
|
const { output } = loadConfig();
|
|
17
19
|
const { noticed = [] } = readJson('notice.json', {});
|
|
18
20
|
const open = listProposals('open');
|
|
19
|
-
const fresh = open.filter((p) => !noticed.includes(p.id))
|
|
21
|
+
const fresh = open.filter((p) => !noticed.includes(p.id)).filter((p) => repoOf(p.repo) === repo ||
|
|
22
|
+
readJson(`batches/${p.batch}/batch.json`, { sessions: [] }).sessions.some((s) => repoOf(s.cwd, s.repo) === repo));
|
|
20
23
|
if (!fresh.length) return '';
|
|
21
24
|
|
|
22
25
|
const shown = open.filter((p) => noticed.includes(p.id) || (fresh.includes(p) && linkFor(p, output)));
|
|
@@ -28,9 +31,9 @@ export function noticeLine() {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// A session-start hook must never break the session, so any failure prints nothing.
|
|
31
|
-
export function notice({ json }) {
|
|
34
|
+
export function notice({ json, cwd }) {
|
|
32
35
|
let line = '';
|
|
33
|
-
try { line = noticeLine(); } catch {}
|
|
36
|
+
try { line = noticeLine(cwd); } catch {}
|
|
34
37
|
if (!line) return;
|
|
35
38
|
console.log(json ? JSON.stringify({ systemMessage: line }) : line);
|
|
36
39
|
}
|
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)], { cwd: ctx.cwd }, (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", "--cwd", ctx.cwd], (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
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Repository identity and opt-in live in Git's shared metadata, not in a checkout.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const options = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
6
|
+
const KEY = 'agentic-retro.enabled';
|
|
7
|
+
|
|
8
|
+
function commonDir(cwd) {
|
|
9
|
+
const path = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], options).replace(/\n$/, '');
|
|
10
|
+
return resolve(cwd, path);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function repoOf(cwd, fallback = cwd) {
|
|
14
|
+
try {
|
|
15
|
+
// Git lists the shared identity first: the main checkout or its separate metadata directory.
|
|
16
|
+
const worktrees = execFileSync('git', ['--git-dir', commonDir(cwd), 'worktree', 'list', '--porcelain', '-z'], options);
|
|
17
|
+
const main = worktrees.split('\0').find((field) => field.startsWith('worktree '));
|
|
18
|
+
return main ? main.slice('worktree '.length) : fallback;
|
|
19
|
+
} catch {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function projectEnabled(cwd) {
|
|
25
|
+
if (!cwd) return false;
|
|
26
|
+
try {
|
|
27
|
+
// Read only the shared config: global and per-worktree overrides cannot enable a project.
|
|
28
|
+
return execFileSync('git', ['config', '--file', join(commonDir(cwd), 'config'), '--bool', '--get', KEY], options).trim() === 'true';
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function project(action, cwd = process.cwd()) {
|
|
35
|
+
if (!['enable', 'disable', 'status'].includes(action)) throw new Error('usage: agentic-retro project enable|disable|status [--cwd <dir>]');
|
|
36
|
+
if (action !== 'status') {
|
|
37
|
+
let dir;
|
|
38
|
+
try { dir = commonDir(cwd); } catch { throw new Error('project enable/disable needs a Git repository'); }
|
|
39
|
+
execFileSync('git', ['config', '--file', join(dir, 'config'), KEY, String(action === 'enable')], options);
|
|
40
|
+
}
|
|
41
|
+
console.log(projectEnabled(cwd) ? 'enabled' : 'disabled');
|
|
42
|
+
}
|
package/cli/lib/queue.js
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
// The queue: finished sessions wait here until their group fills a batch.
|
|
2
|
-
import {
|
|
3
|
-
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { createReadStream, readdirSync } from 'node:fs';
|
|
4
3
|
import { homedir } from 'node:os';
|
|
5
|
-
import { join, basename
|
|
4
|
+
import { join, basename } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline';
|
|
6
6
|
import { readJson, writeJson } from './paths.js';
|
|
7
7
|
import { loadConfig } from './config.js';
|
|
8
8
|
import { startBatch } from './runner.js';
|
|
9
|
-
|
|
10
|
-
function repoOf(cwd, fallback = cwd) {
|
|
11
|
-
try {
|
|
12
|
-
const options = { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] };
|
|
13
|
-
const commonDir = execFileSync('git', ['-C', cwd, 'rev-parse', '--git-common-dir'], options).replace(/\n$/, '');
|
|
14
|
-
// Git lists the shared identity first: the main checkout or its separate metadata directory.
|
|
15
|
-
const worktrees = execFileSync('git', ['--git-dir', resolve(cwd, commonDir), 'worktree', 'list', '--porcelain', '-z'], options);
|
|
16
|
-
const main = worktrees.split('\0').find((field) => field.startsWith('worktree '));
|
|
17
|
-
return main ? main.slice('worktree '.length) : fallback;
|
|
18
|
-
} catch {
|
|
19
|
-
return fallback;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
9
|
+
import { projectEnabled, repoOf } from './project.js';
|
|
22
10
|
|
|
23
11
|
// Codex sometimes ends a session with a null transcript_path, so look for its rollout file.
|
|
24
12
|
function findCodexRollout(sessionId) {
|
|
@@ -41,11 +29,29 @@ function harnessOf(transcript) {
|
|
|
41
29
|
return undefined;
|
|
42
30
|
}
|
|
43
31
|
|
|
32
|
+
async function isPiChildTranscript(transcript) {
|
|
33
|
+
if (!transcript) return false;
|
|
34
|
+
try {
|
|
35
|
+
const lines = createInterface({ input: createReadStream(transcript), crlfDelay: Infinity });
|
|
36
|
+
for await (const line of lines) {
|
|
37
|
+
let entry;
|
|
38
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
39
|
+
if (entry?.type !== 'session') continue;
|
|
40
|
+
return Object.hasOwn(entry, 'parentSession');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
export async function enqueue({ harness, sessionId, transcript, cwd }) {
|
|
45
49
|
if (process.env.AGENTIC_RETRO_RUN === '1') return; // batch agents never queue themselves
|
|
50
|
+
if (!projectEnabled(cwd)) return;
|
|
46
51
|
transcript ||= findCodexRollout(sessionId) ?? null;
|
|
47
52
|
harness ||= harnessOf(transcript);
|
|
48
53
|
if (!harness || !sessionId || !cwd) throw new Error('enqueue needs a harness, session, and cwd');
|
|
54
|
+
if (harness === 'pi' && await isPiChildTranscript(transcript)) return;
|
|
49
55
|
|
|
50
56
|
const queue = readJson('queue.json', []).map((s) => ({ ...s, repo: repoOf(s.cwd, s.repo) }));
|
|
51
57
|
if (queue.some((s) => s.sessionId === sessionId)) return writeJson('queue.json', queue);
|
|
@@ -54,7 +60,7 @@ export async function enqueue({ harness, sessionId, transcript, cwd }) {
|
|
|
54
60
|
|
|
55
61
|
const { batch } = loadConfig();
|
|
56
62
|
const sameGroup = (s) => batch.group === 'global' || s.repo === entry.repo;
|
|
57
|
-
const group = queue.filter(sameGroup);
|
|
63
|
+
const group = queue.filter((s) => sameGroup(s) && projectEnabled(s.repo));
|
|
58
64
|
if (group.length < batch.size) return writeJson('queue.json', queue);
|
|
59
65
|
|
|
60
66
|
// Save first and remove after, so a failed hand-off leaves the sessions queued.
|
package/cli/lib/setup.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-retro",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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/setup/SKILL.md
CHANGED
|
@@ -25,7 +25,7 @@ Offer the presets below for each question, and let the user type a different ans
|
|
|
25
25
|
1. **Batch size** – sessions per batch, default 5
|
|
26
26
|
2. **Grouping** – `repo` (default) or `global`
|
|
27
27
|
3. **Runner harness** – `claude`, `codex`, or `pi`; add `herdr` to the choices only if `command -v herdr` finds it
|
|
28
|
-
4. **Model** – `opus` or `default`; `opus` applies to Claude, Codex and Pi use their
|
|
28
|
+
4. **Model** – `opus` or `default`; `opus` applies to Claude, Codex and Pi use their default unless given a model name they understand (for example `o3`)
|
|
29
29
|
5. **Runner permissions** – how much the batch agent can do without asking:
|
|
30
30
|
- `auto` (default) – the harness's own auto mode
|
|
31
31
|
- `scoped` – file edits, plus only `git`, `gh`, and `agentic-retro` commands
|
|
@@ -67,6 +67,14 @@ Never edit `config.json` by hand. `config set` is the only path that keeps the s
|
|
|
67
67
|
|
|
68
68
|
Run `command -v pi`. If it finds Pi, ask whether to install the agentic-retro extension for it. On yes, run `agentic-retro install-pi`.
|
|
69
69
|
|
|
70
|
+
## Enable this repository
|
|
71
|
+
|
|
72
|
+
Every repository starts disabled. Ask whether the user wants to enable collection in the current repository.
|
|
73
|
+
|
|
74
|
+
On yes, run `agentic-retro project enable` from that repository. All its worktrees share this untracked Git setting.
|
|
75
|
+
|
|
76
|
+
On no, leave the repository disabled. Global configuration alone does not enable collection.
|
|
77
|
+
|
|
70
78
|
## Finish
|
|
71
79
|
|
|
72
80
|
Run `agentic-retro config list` and show the user the result.
|