agentic-retro 0.1.3 → 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 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)"] --> B["CLI adds session to queue"]
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["Stop"]
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 in each harness queues the session (`agentic-retro enqueue`)
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
- Everything lives under `~/.agentic-retro/` (override with `AGENTIC_RETRO_HOME`): `config.json`, `queue.json`, `ledger.json`, `metrics.jsonl`, per-batch digests, and report files.
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
@@ -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
  }
@@ -78,7 +78,7 @@ export default function (pi: ExtensionAPI) {
78
78
  pi.registerCommand("agentic-retro", {
79
79
  description: "Run the package-local agentic-retro CLI",
80
80
  handler: async (args, ctx) => {
81
- execFile(process.execPath, [CLI, ...argv(args)], (error, stdout, stderr) => {
81
+ execFile(process.execPath, [CLI, ...argv(args)], { cwd: ctx.cwd }, (error, stdout, stderr) => {
82
82
  const output = (stdout || stderr).trim();
83
83
  if (output) ctx.ui.notify(output, error ? "error" : "info");
84
84
  });
@@ -121,7 +121,7 @@ export default function (pi: ExtensionAPI) {
121
121
  });
122
122
 
123
123
  pi.on("session_start", (_event, ctx) => {
124
- execFile(process.execPath, [CLI, "notice"], (error, stdout) => {
124
+ execFile(process.execPath, [CLI, "notice", "--cwd", ctx.cwd], (error, stdout) => {
125
125
  const line = stdout?.trim();
126
126
  if (!error && line) ctx.ui.notify(line, "info");
127
127
  });
@@ -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,25 +1,12 @@
1
1
  // The queue: finished sessions wait here until their group fills a batch.
2
- import { execFileSync } from 'node:child_process';
3
2
  import { createReadStream, readdirSync } from 'node:fs';
4
3
  import { homedir } from 'node:os';
5
- import { join, basename, resolve } from 'node:path';
4
+ import { join, basename } from 'node:path';
6
5
  import { createInterface } from 'node:readline';
7
6
  import { readJson, writeJson } from './paths.js';
8
7
  import { loadConfig } from './config.js';
9
8
  import { startBatch } from './runner.js';
10
-
11
- function repoOf(cwd, fallback = cwd) {
12
- try {
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;
19
- } catch {
20
- return fallback;
21
- }
22
- }
9
+ import { projectEnabled, repoOf } from './project.js';
23
10
 
24
11
  // Codex sometimes ends a session with a null transcript_path, so look for its rollout file.
25
12
  function findCodexRollout(sessionId) {
@@ -60,6 +47,7 @@ async function isPiChildTranscript(transcript) {
60
47
 
61
48
  export async function enqueue({ harness, sessionId, transcript, cwd }) {
62
49
  if (process.env.AGENTIC_RETRO_RUN === '1') return; // batch agents never queue themselves
50
+ if (!projectEnabled(cwd)) return;
63
51
  transcript ||= findCodexRollout(sessionId) ?? null;
64
52
  harness ||= harnessOf(transcript);
65
53
  if (!harness || !sessionId || !cwd) throw new Error('enqueue needs a harness, session, and cwd');
@@ -72,7 +60,7 @@ export async function enqueue({ harness, sessionId, transcript, cwd }) {
72
60
 
73
61
  const { batch } = loadConfig();
74
62
  const sameGroup = (s) => batch.group === 'global' || s.repo === entry.repo;
75
- const group = queue.filter(sameGroup);
63
+ const group = queue.filter((s) => sameGroup(s) && projectEnabled(s.repo));
76
64
  if (group.length < batch.size) return writeJson('queue.json', queue);
77
65
 
78
66
  // Save first and remove after, so a failed hand-off leaves the sessions queued.
package/cli/lib/setup.js CHANGED
@@ -50,5 +50,5 @@ export async function setup() {
50
50
 
51
51
  rl.close();
52
52
  saveConfig(config);
53
- process.stdout.write('\nSaved config.json\n');
53
+ process.stdout.write('\nSaved config.json\nRepositories remain disabled until you run: agentic-retro project enable\n');
54
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-retro",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "agentic-retrospective: reviews finished agent coding sessions and proposes improvements",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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 own default unless given a model name they understand (for example `o3`)
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.