projecta-rrr 1.22.1 → 1.22.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/CHANGELOG.md CHANGED
@@ -4,6 +4,67 @@ All notable changes to RRR will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.22.3] - 2026-04-19
8
+
9
+ **Patch: postinstall full provision + tool-disallow real-payload subagent detection + hook wiring.**
10
+
11
+ ### Fixed
12
+ - **`bin/install.js` postinstall now full-provisions** (was `--hud-only`). Fresh `npm install -g projecta-rrr` lands 47 commands, 13 agents, 18 hooks, plus settings.json with v1.22 hooks pre-wired (default-safe). Previously users had to run `npx projecta-rrr install` separately to get commands/agents.
13
+ - **v1.22 hooks now auto-wired** into `~/.claude/settings.json` on install:
14
+ - `PreToolUse` matcher `Task|Agent` → `hooks/model-router.js`
15
+ - `PreToolUse` matcher `Read|Grep|Glob` → `hooks/tool-disallow.js`
16
+ - `PostToolUse` matcher `Edit|MultiEdit` → `hooks/edit-batching-nudge.js`
17
+ - Defaults seeded: `settings.rrr.model_router: "static"` (v1.21 behavior), `settings.rrr.tok_disallow: false` (opt-in).
18
+ - Idempotent — re-install does not duplicate entries.
19
+ - Previously hook files copied but not wired (only `runHostedInstallOrchestrator()` wired them, which only runs with `--enable-hosted`).
20
+ - **`hooks/tool-disallow.js` subagent detection** now uses real Claude Code payload schema. Previous heuristic checked `input.agent_name` which does NOT exist in PreToolUse payloads (UAT-verified schema: `{session_id, transcript_path, cwd, permission_mode, hook_event_name, tool_name, tool_input, tool_use_id}`). New mechanism: read tail of `transcript_path` and check `isSidechain: true` to detect subagent context. Fallback: legacy `agent_name` field for forward-compat. Fail-CLOSED if transcript unreadable (over-block better than under-block when goal is token discipline).
21
+
22
+ ### Operator data populated this session
23
+ - **`installations` table**: 5 rows (was 0) — populated from existing `repos.installation_id` data.
24
+ - **`repos.github_repo_id` backfill**: 24 of 59 real repos populated via `gh api /repos/PA-Ai-Team/<slug>`. Remaining 35 are inaccessible to the user PAT (likely deleted, archived, or upstream forks like OpenHands/letta — webhook routing for them is unreachable anyway, so NULL is correct).
25
+
26
+ ### Verified in UAT
27
+ - Fresh `HOME=tmp npm install -g projecta-rrr@1.22.3 --prefix=tmp` lands 47 commands + 13 agents + 18 hooks + 3 wired hook entries with safe defaults. Re-install is idempotent (no duplicate entries).
28
+ - `tool-disallow`: main-thread Read with synthetic transcript (`isSidechain:false`) → BLOCK exit 2; subagent Read with `isSidechain:true` → ALLOW exit 0.
29
+ - 239/239 tests pass; `prepublish:check` clean.
30
+
31
+ ### Known followups
32
+ - 35 repos with NULL `github_repo_id` (private/deleted) — operator can run via App-installation token (broader scope) if needed; for now webhook safely skips them with `{"skipped":"unindexed-repo"}`.
33
+
34
+ ## [1.22.2] - 2026-04-19
35
+
36
+ **Patch: dynamic routing now actually fires in Claude Code (Opus 4.7+).**
37
+
38
+ ### Fixed
39
+ - **Phase 83 model-router matcher**: Claude Code (Opus 4.7+) emits subagent dispatches as `tool_name: "Agent"`, not `"Task"`. v1.22.0/.1 hardcoded `"Task"` in both:
40
+ 1. `hooks/model-router.js` internal gate (`if (toolName !== "Task") return emitAllow()`)
41
+ 2. `rrr/lib/install-hooks-wiring.js` matcher (`{ matcher: "Task", ... }`)
42
+ Result: hook never fired for any Agent dispatch in Opus 4.7+ sessions, so dynamic routing was a no-op even with `settings.rrr.model_router: "dynamic"`.
43
+
44
+ v1.22.2 accepts both `Task` and `Agent`. Live UAT verified end-to-end:
45
+ ```
46
+ {"ts":"2026-04-19T05:46:21Z","event":"dispatch","tool_name":"Agent",
47
+ "agent_name":"rrr-explore","tier":"haiku","reason":"dispatch"}
48
+ ```
49
+ Token usage on rrr-explore call: ~10.4k (haiku) vs ~13.2k baseline (general-purpose, sonnet/inherit) — ~21% reduction from tier downgrade alone, on a single dispatch.
50
+
51
+ ### Verified in UAT (v1.21 hosted webhook + Phase 80 App-level)
52
+ - **Hosted Fly app redeployed** with v1.22 code (was running pre-Phase-80 image — `/webhooks/github-app` returned 404 until `fly deploy`).
53
+ - **Phase 80 webhook end-to-end**:
54
+ - Ping: 200 OK
55
+ - Valid HMAC push (with `installation_id` lookup): 202 + `{queued:true, job_id}`
56
+ - Replay (same delivery_id): 200 + `{duplicate:true}` ✓ dedup
57
+ - Bad signature: 401 `signature_invalid`
58
+ - Non-default branch: 200 `{skipped:"non-default-branch"}`
59
+ - Unindexed repo (NULL `github_repo_id`): 200 `{skipped:"unindexed-repo"}`
60
+ - **Neon migration 014 applied**: `installations` table populated with 5 rows from existing `repos.installation_id` data.
61
+ - **`installations` table**: 5 rows under `team_id=pa-ai-team`. Largest installation: 125117864 (29 repos).
62
+
63
+ ### Known followups
64
+ - **Backfill `repos.github_repo_id`** for all 62 repos via `gh api /repos/<full_name>`. Without this, App-level webhook deliveries hit "unindexed-repo" skip path. Operator-actionable.
65
+ - **Postinstall `--hud-only`** still requires separate `npx projecta-rrr install` for full provision. v1.22.x followup.
66
+ - **`tool-disallow` subagent exemption heuristic** uses `input.agent_name` — this field is NOT present in real Claude Code PreToolUse payload (UAT-verified payload schema has `tool_name`, `tool_input`, `session_id`, `transcript_path`, `cwd`, `permission_mode`, `tool_use_id`). Subagent context detection needs different mechanism.
67
+
7
68
  ## [1.22.1] - 2026-04-19
8
69
 
9
70
  **Patch: fixes v1.22.0 fresh install regression.**
package/bin/install.js CHANGED
@@ -1730,6 +1730,47 @@ function install(isGlobal) {
1730
1730
  console.log(` ${green}✓${reset} Installed hooks`);
1731
1731
  }
1732
1732
 
1733
+ // v1.22.3: wire Phase 83 (model-router), Phase 85 (tool-disallow + edit-batching-nudge)
1734
+ // PreToolUse/PostToolUse hooks. Idempotent — checks for existing entries before
1735
+ // adding. Defaults are SAFE (settings.rrr.tok_disallow=false; model_router=static).
1736
+ // Previously these were only wired when --enable-hosted ran the hosted orchestrator,
1737
+ // so users got the hooks on disk but no settings.json registration.
1738
+ if (bashStatus.available && fs.existsSync(path.join(claudeDir, 'hooks', 'model-router.js'))) {
1739
+ settings.hooks = settings.hooks || {};
1740
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
1741
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
1742
+ settings.rrr = settings.rrr || {};
1743
+
1744
+ const wireOnce = (chain, matcher, hookCmd, label) => {
1745
+ const exists = chain.some((entry) =>
1746
+ entry && Array.isArray(entry.hooks) && entry.hooks.some(
1747
+ (h) => h && typeof h.command === 'string' && h.command.includes(label)
1748
+ )
1749
+ );
1750
+ if (!exists) {
1751
+ chain.push({ matcher, hooks: [{ type: 'command', command: hookCmd }] });
1752
+ return true;
1753
+ }
1754
+ return false;
1755
+ };
1756
+
1757
+ let wiredCount = 0;
1758
+ if (wireOnce(settings.hooks.PreToolUse, 'Task|Agent', '$HOME/.claude/hooks/model-router.js', 'model-router.js')) wiredCount++;
1759
+ if (fs.existsSync(path.join(claudeDir, 'hooks', 'tool-disallow.js'))) {
1760
+ if (wireOnce(settings.hooks.PreToolUse, 'Read|Grep|Glob', 'node $HOME/.claude/hooks/tool-disallow.js', 'tool-disallow.js')) wiredCount++;
1761
+ }
1762
+ if (fs.existsSync(path.join(claudeDir, 'hooks', 'edit-batching-nudge.js'))) {
1763
+ if (wireOnce(settings.hooks.PostToolUse, 'Edit|MultiEdit', 'node $HOME/.claude/hooks/edit-batching-nudge.js', 'edit-batching-nudge.js')) wiredCount++;
1764
+ }
1765
+ if (settings.rrr.model_router === undefined) settings.rrr.model_router = 'static';
1766
+ if (settings.rrr.tok_disallow === undefined) settings.rrr.tok_disallow = false;
1767
+ if (wiredCount > 0) {
1768
+ console.log(` ${green}✓${reset} Wired ${wiredCount} v1.22 hook(s) (settings.rrr.model_router=static, tok_disallow=false — opt-in)`);
1769
+ } else {
1770
+ console.log(` ${dim}✓${reset} v1.22 hooks already wired`);
1771
+ }
1772
+ }
1773
+
1733
1774
  const statuslineCommand = isGlobal
1734
1775
  ? '$HOME/.claude/hooks/statusline.sh'
1735
1776
  : `${localDirName}/hooks/statusline.sh`;
@@ -85,9 +85,12 @@ function main() {
85
85
  try { input = JSON.parse(raw); }
86
86
  catch (_e) { return emitAllow('router: stdin not JSON'); }
87
87
 
88
- // Only act on the Task tool — every other tool is pass-through.
88
+ // Only act on the subagent-dispatch tool — every other tool is pass-through.
89
+ // Claude Code uses either "Task" (Anthropic API name, most builds) or "Agent"
90
+ // (Opus 4.7+ builds) for subagent dispatch. Accept both — confirmed via
91
+ // v1.22.2 UAT live-session capture (/tmp/uat-hook-log.jsonl).
89
92
  const toolName = input.tool_name;
90
- if (toolName !== 'Task') return emitAllow();
93
+ if (toolName !== 'Task' && toolName !== 'Agent') return emitAllow();
91
94
 
92
95
  const toolInput = (input.tool_input && typeof input.tool_input === 'object') ? input.tool_input : {};
93
96
 
@@ -18,9 +18,16 @@
18
18
  * and scripts/test-install-smoke.js).
19
19
  * - env RRR_TOK_DISALLOW=off → kill-switch override.
20
20
  *
21
- * Subagent exemption:
22
- * - PreToolUse JSON payload includes `agent_name` for subagent calls.
23
- * Empty/missing = main thread.
21
+ * Subagent exemption (v1.22.3 fix — UAT-verified payload schema):
22
+ * - Real Claude Code PreToolUse payload does NOT include `agent_name`.
23
+ * Schema is: {session_id, transcript_path, cwd, permission_mode,
24
+ * hook_event_name, tool_name, tool_input, tool_use_id}
25
+ * - Subagent context is detected by reading the LAST line of
26
+ * `transcript_path` and checking `isSidechain: true`. Sidechain entries
27
+ * are subagent calls; main thread is `isSidechain: false`.
28
+ * - Fallback: if transcript can't be read or parsed, treat as main thread
29
+ * (fail-CLOSED for the BLOCK case — better to over-block than under-block
30
+ * when the goal is token discipline).
24
31
  *
25
32
  * Settings location (read-only): ~/.claude/settings.json (then project
26
33
  * .claude/settings.json — project wins). Never modified here; wiring is
@@ -66,6 +73,43 @@ function isAgentExempt(agentName, exemptList) {
66
73
  return true;
67
74
  }
68
75
 
76
+ /**
77
+ * Detect subagent context by reading the tail of the Claude Code transcript
78
+ * and checking for `isSidechain: true` on recent entries.
79
+ *
80
+ * Real Claude Code PreToolUse payload includes `transcript_path` but NOT
81
+ * `agent_name`. The transcript JSONL has one entry per turn with isSidechain
82
+ * marking subagent context (UAT-verified in v1.22.3).
83
+ *
84
+ * Returns: 'subagent' | 'main' | 'unknown'
85
+ */
86
+ function detectThreadFromTranscript(transcriptPath) {
87
+ if (!transcriptPath || typeof transcriptPath !== 'string') return 'unknown';
88
+ try {
89
+ if (!fs.existsSync(transcriptPath)) return 'unknown';
90
+ // Read last ~64KB only (transcripts can be large; we only need recent context).
91
+ const stat = fs.statSync(transcriptPath);
92
+ const fd = fs.openSync(transcriptPath, 'r');
93
+ const tailSize = Math.min(stat.size, 65536);
94
+ const buf = Buffer.alloc(tailSize);
95
+ fs.readSync(fd, buf, 0, tailSize, Math.max(0, stat.size - tailSize));
96
+ fs.closeSync(fd);
97
+ const lines = buf.toString('utf8').split('\n').filter(Boolean);
98
+ // Walk backward — the most recent non-tool-result entry is the current turn.
99
+ for (let i = lines.length - 1; i >= 0; i--) {
100
+ try {
101
+ const e = JSON.parse(lines[i]);
102
+ if (typeof e.isSidechain === 'boolean') {
103
+ return e.isSidechain ? 'subagent' : 'main';
104
+ }
105
+ } catch { /* skip malformed line */ }
106
+ }
107
+ return 'unknown';
108
+ } catch {
109
+ return 'unknown';
110
+ }
111
+ }
112
+
69
113
  function decideMode(settings) {
70
114
  // Env kill-switch wins.
71
115
  if ((process.env.RRR_TOK_DISALLOW || '').toLowerCase() === 'off') return 'off';
@@ -98,12 +142,18 @@ function main() {
98
142
  // a misconfigured settings.json never breaks the installer.
99
143
  if (process.env.RRR_INTERNAL_TOOL === '1') { process.exit(0); }
100
144
 
101
- // Subagent exemption — agent_name set = subagent context.
102
- const agentName = input.agent_name || (input.session && input.session.agent_name) || '';
145
+ // Subagent exemption — v1.22.3: detect via transcript_path + isSidechain.
146
+ // Real Claude Code payload schema does NOT include input.agent_name.
147
+ // Fallback to legacy input.agent_name field for forward-compat with future
148
+ // Claude Code versions that may add it.
149
+ const legacyAgentName = input.agent_name || (input.session && input.session.agent_name) || '';
150
+ const thread = legacyAgentName ? 'subagent' : detectThreadFromTranscript(input.transcript_path);
103
151
  const settings = loadSettings();
104
152
  const exemptList = settings.tok_disallow_exempt;
105
153
 
106
- if (isAgentExempt(agentName, exemptList)) { process.exit(0); }
154
+ // Subagent (sidechain) always exempt. Unknown → treat as main (fail-CLOSED).
155
+ if (thread === 'subagent') { process.exit(0); }
156
+ if (legacyAgentName && isAgentExempt(legacyAgentName, exemptList)) { process.exit(0); }
107
157
 
108
158
  // Explicit per-name allow (rare: when main-thread invocation of a
109
159
  // specific named context should still be permitted).
@@ -127,5 +177,6 @@ module.exports = {
127
177
  BLOCKED_TOOLS,
128
178
  decideMode,
129
179
  isAgentExempt,
180
+ detectThreadFromTranscript,
130
181
  buildMessage,
131
182
  };
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "projecta-rrr",
3
- "version": "1.22.1",
3
+ "version": "1.22.3",
4
4
  "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by Projecta.ai",
5
5
  "bin": {
6
6
  "projecta-rrr": "bin/install.js",
7
7
  "projecta-rrr-hosted-setup": "bin/hosted-setup.js"
8
8
  },
9
9
  "scripts": {
10
- "postinstall": "node bin/install.js --hud-only --global && node scripts/register-mcp.js",
10
+ "postinstall": "node bin/install.js --global --yes --skip-optimization && node scripts/register-mcp.js",
11
11
  "watch": "node watcher/index.js",
12
12
  "watch:verbose": "node watcher/index.js --verbose",
13
13
  "watch:claude-code": "node watcher/watchers/claude-code.js --verbose",
@@ -210,7 +210,10 @@ function wireToolRedirect({ repoHooksDir, userHooksDir, userSettingsPath, dryRun
210
210
  */
211
211
  function buildModelRouterEntry(hookAbsPath) {
212
212
  return {
213
- matcher: 'Task',
213
+ // Claude Code uses either "Task" (most builds) or "Agent" (Opus 4.7+) as
214
+ // the subagent dispatch tool name. Match both via regex alternation —
215
+ // confirmed via v1.22.2 UAT live-session capture.
216
+ matcher: 'Task|Agent',
214
217
  hooks: [
215
218
  { type: 'command', command: `node ${hookAbsPath}` },
216
219
  ],