create-walle 0.9.30 → 0.9.32

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.
Files changed (112) hide show
  1. package/package.json +1 -1
  2. package/template/claude-task-manager/api-prompts.js +410 -5
  3. package/template/claude-task-manager/api-reviews.js +251 -3
  4. package/template/claude-task-manager/approval-agent.js +322 -24
  5. package/template/claude-task-manager/db.js +77 -13
  6. package/template/claude-task-manager/docs/codex-app-server-approvals.md +35 -1
  7. package/template/claude-task-manager/docs/postgres-concurrency-dgx-spark.html +407 -0
  8. package/template/claude-task-manager/docs/prompt-manager-redesign-proposal.html +667 -0
  9. package/template/claude-task-manager/docs/resume-ux-redesign.html +493 -0
  10. package/template/claude-task-manager/docs/review-redesign-proposal.html +892 -0
  11. package/template/claude-task-manager/docs/unified-auto-approver-proposal.html +520 -0
  12. package/template/claude-task-manager/git-utils.js +164 -26
  13. package/template/claude-task-manager/lib/agent-capabilities.js +26 -5
  14. package/template/claude-task-manager/lib/agent-cli-cache.js +1 -1
  15. package/template/claude-task-manager/lib/agent-presets.js +12 -0
  16. package/template/claude-task-manager/lib/agent-version-service.js +650 -0
  17. package/template/claude-task-manager/lib/approval-drift.js +57 -0
  18. package/template/claude-task-manager/lib/approval-hook.js +13 -0
  19. package/template/claude-task-manager/lib/auth-rules.js +6 -0
  20. package/template/claude-task-manager/lib/broadcast-payload-memo.js +109 -0
  21. package/template/claude-task-manager/lib/coding-agent-models.js +89 -8
  22. package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +10 -1
  23. package/template/claude-task-manager/lib/document-review.js +75 -1
  24. package/template/claude-task-manager/lib/escalation-review.js +33 -3
  25. package/template/claude-task-manager/lib/headless-term-service.js +54 -9
  26. package/template/claude-task-manager/lib/mcp-risk.js +128 -0
  27. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +51 -2
  28. package/template/claude-task-manager/lib/native-agent-model-args.js +149 -0
  29. package/template/claude-task-manager/lib/opencode-conversation-store.js +269 -0
  30. package/template/claude-task-manager/lib/permission-match.js +77 -5
  31. package/template/claude-task-manager/lib/read-pool-client.js +15 -6
  32. package/template/claude-task-manager/lib/restore-backoff.js +82 -0
  33. package/template/claude-task-manager/lib/runtime-registry.js +1 -0
  34. package/template/claude-task-manager/lib/scheduled-wake.js +42 -0
  35. package/template/claude-task-manager/lib/session-capture.js +33 -6
  36. package/template/claude-task-manager/lib/session-history.js +60 -3
  37. package/template/claude-task-manager/lib/session-jobs.js +19 -0
  38. package/template/claude-task-manager/lib/session-restore.js +5 -4
  39. package/template/claude-task-manager/lib/session-standup.js +2 -0
  40. package/template/claude-task-manager/lib/session-stream.js +9 -0
  41. package/template/claude-task-manager/lib/state-sync/cell-diff.js +31 -4
  42. package/template/claude-task-manager/lib/state-sync/restore-frame-hold.js +39 -0
  43. package/template/claude-task-manager/lib/tui-input-modes.js +40 -0
  44. package/template/claude-task-manager/lib/walle-ctm-history.js +103 -36
  45. package/template/claude-task-manager/lib/walle-session-model-catalog.js +7 -1
  46. package/template/claude-task-manager/lib/walle-token-chip.js +65 -0
  47. package/template/claude-task-manager/package.json +1 -1
  48. package/template/claude-task-manager/providers/claude-code.js +20 -1
  49. package/template/claude-task-manager/providers/codex-mcp-extract.js +33 -0
  50. package/template/claude-task-manager/providers/codex-mcp.js +23 -20
  51. package/template/claude-task-manager/providers/codex.js +65 -1
  52. package/template/claude-task-manager/public/css/prompts.css +345 -0
  53. package/template/claude-task-manager/public/css/reviews.css +248 -40
  54. package/template/claude-task-manager/public/css/walle-session.css +90 -26
  55. package/template/claude-task-manager/public/index.html +2031 -428
  56. package/template/claude-task-manager/public/js/document-review-links.js +255 -13
  57. package/template/claude-task-manager/public/js/file-context-menu.js +185 -0
  58. package/template/claude-task-manager/public/js/message-renderer.js +46 -1
  59. package/template/claude-task-manager/public/js/mobile-review-core.js +54 -0
  60. package/template/claude-task-manager/public/js/prompt-diff.js +55 -0
  61. package/template/claude-task-manager/public/js/prompt-editor-chrome.js +108 -0
  62. package/template/claude-task-manager/public/js/prompt-editor-modes.js +87 -0
  63. package/template/claude-task-manager/public/js/prompt-organize.js +179 -0
  64. package/template/claude-task-manager/public/js/prompt-reuse.js +77 -0
  65. package/template/claude-task-manager/public/js/prompts.js +683 -215
  66. package/template/claude-task-manager/public/js/resume-state.js +186 -0
  67. package/template/claude-task-manager/public/js/reviews.js +402 -121
  68. package/template/claude-task-manager/public/js/screenshot-router.js +91 -0
  69. package/template/claude-task-manager/public/js/session-search-utils.js +81 -0
  70. package/template/claude-task-manager/public/js/state-sync-client.js +134 -17
  71. package/template/claude-task-manager/public/js/stream-view.js +93 -20
  72. package/template/claude-task-manager/public/js/walle-session.js +183 -32
  73. package/template/claude-task-manager/public/js/walle.js +21 -9
  74. package/template/claude-task-manager/public/m/app.css +117 -0
  75. package/template/claude-task-manager/public/m/app.js +309 -3
  76. package/template/claude-task-manager/public/m/index.html +56 -1
  77. package/template/claude-task-manager/server.js +724 -305
  78. package/template/claude-task-manager/workers/db-owner-worker.js +8 -0
  79. package/template/claude-task-manager/workers/read-pool-worker.js +4 -0
  80. package/template/claude-task-manager/workers/state-detectors/claude-code.js +10 -1
  81. package/template/docs/proposals/2026-06-23-local-model-right-path.html +340 -0
  82. package/template/docs/proposals/2026-06-24-qlora-done-right.md +255 -0
  83. package/template/package.json +1 -1
  84. package/template/wall-e/api-walle.js +21 -3
  85. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +27 -4
  86. package/template/wall-e/brain.js +50 -5
  87. package/template/wall-e/chat/force-compact.js +73 -0
  88. package/template/wall-e/chat.js +137 -9
  89. package/template/wall-e/coding/action-memory-policy.js +120 -1
  90. package/template/wall-e/coding/compaction-service.js +10 -3
  91. package/template/wall-e/coding/model-router.js +116 -0
  92. package/template/wall-e/coding-orchestrator.js +303 -11
  93. package/template/wall-e/coding-prompts.js +1 -1
  94. package/template/wall-e/docs/skill-self-heal-design.html +429 -0
  95. package/template/wall-e/llm/client.js +115 -6
  96. package/template/wall-e/llm/codex-cli.js +71 -23
  97. package/template/wall-e/llm/codex-cli.plugin.json +1 -0
  98. package/template/wall-e/llm/default-fallback.js +71 -11
  99. package/template/wall-e/llm/mlx-worker.js +0 -0
  100. package/template/wall-e/llm/mlx.js +46 -12
  101. package/template/wall-e/llm/provider-error.js +9 -3
  102. package/template/wall-e/llm/provider-health-state.js +561 -11
  103. package/template/wall-e/llm/registry.js +6 -0
  104. package/template/wall-e/llm/routing-policy.js +255 -25
  105. package/template/wall-e/llm/tool-call-validator.js +156 -0
  106. package/template/wall-e/skills/skill-dispatch-decision.js +82 -2
  107. package/template/wall-e/skills/skill-planner.js +72 -5
  108. package/template/wall-e/tools/command-registry.js +34 -0
  109. package/template/wall-e/training/gemma-e4b-qlora.js +197 -1
  110. package/template/wall-e/training/mlx_lora_launch.py +196 -0
  111. package/template/wall-e/training/training-coexistence.sh +87 -0
  112. package/template/wall-e/training/training-mem-guard.sh +76 -0
@@ -0,0 +1,520 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Unified Auto-Approver — Design &amp; Proposal</title>
7
+ <style>
8
+ :root{
9
+ --bg:#1a1b26; --bg-light:#1f2335; --bg-elev:#24283b; --bg-card:#1e2030;
10
+ --fg:#c0caf5; --fg-dim:#9aa5ce; --fg-muted:#737aa2;
11
+ --accent:#7aa2f7; --green:#9ece6a; --yellow:#e0af68; --red:#f7768e;
12
+ --purple:#bb9af7; --cyan:#7dcfff; --orange:#ff9e64;
13
+ --border:#2a2e42; --border-l:#3b4261;
14
+ --mono:'SF Mono',ui-monospace,'JetBrains Mono',Menlo,Consolas,monospace;
15
+ --sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Inter,system-ui,sans-serif;
16
+ }
17
+ *{box-sizing:border-box}
18
+ html{scroll-behavior:smooth}
19
+ body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);
20
+ font-size:15px;line-height:1.62;-webkit-font-smoothing:antialiased}
21
+ .wrap{max-width:1080px;margin:0 auto;padding:0 28px 120px}
22
+ header.hero{padding:64px 28px 40px;max-width:1080px;margin:0 auto}
23
+ .kicker{font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:0 0 14px}
24
+ h1{font-size:40px;line-height:1.12;margin:0 0 16px;font-weight:700;letter-spacing:-.02em}
25
+ h1 .u{background:linear-gradient(90deg,var(--accent),var(--purple));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
26
+ .sub{font-size:18px;color:var(--fg-dim);max-width:760px;margin:0}
27
+ .meta{margin-top:22px;font-family:var(--mono);font-size:12.5px;color:var(--fg-muted);display:flex;gap:18px;flex-wrap:wrap}
28
+ .meta b{color:var(--fg-dim);font-weight:600}
29
+ h2{font-size:26px;margin:64px 0 8px;letter-spacing:-.01em;padding-top:14px;border-top:1px solid var(--border)}
30
+ h2 .n{font-family:var(--mono);font-size:15px;color:var(--accent);margin-right:12px;font-weight:600}
31
+ h3{font-size:18px;margin:34px 0 6px;color:var(--fg)}
32
+ h4{font-size:14.5px;margin:22px 0 4px;color:var(--cyan);font-family:var(--mono);font-weight:600}
33
+ p{margin:10px 0}
34
+ a{color:var(--accent);text-decoration:none;border-bottom:1px solid transparent}
35
+ a:hover{border-bottom-color:var(--accent)}
36
+ code{font-family:var(--mono);font-size:.86em;background:var(--bg-elev);color:var(--cyan);
37
+ padding:1.5px 6px;border-radius:5px;border:1px solid var(--border)}
38
+ .lead{font-size:16.5px;color:var(--fg-dim)}
39
+ strong{color:#dbe2ff;font-weight:600}
40
+ em{color:var(--fg);font-style:italic}
41
+ ul,ol{margin:10px 0;padding-left:22px}
42
+ li{margin:6px 0}
43
+ .card{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:22px 24px;margin:20px 0}
44
+ .grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px}
45
+ @media(max-width:760px){.grid2{grid-template-columns:1fr}h1{font-size:31px}}
46
+ .pill{display:inline-block;font-family:var(--mono);font-size:11px;font-weight:600;
47
+ padding:2px 9px;border-radius:20px;letter-spacing:.03em;vertical-align:middle}
48
+ .pill.low{background:rgba(158,206,106,.14);color:var(--green);border:1px solid rgba(158,206,106,.3)}
49
+ .pill.med{background:rgba(224,175,104,.14);color:var(--yellow);border:1px solid rgba(224,175,104,.3)}
50
+ .pill.high{background:rgba(247,118,142,.14);color:var(--red);border:1px solid rgba(247,118,142,.3)}
51
+ .pill.acc{background:rgba(122,162,247,.14);color:var(--accent);border:1px solid rgba(122,162,247,.3)}
52
+ table{width:100%;border-collapse:collapse;margin:18px 0;font-size:13.5px}
53
+ th,td{text-align:left;padding:11px 13px;border-bottom:1px solid var(--border);vertical-align:top}
54
+ th{font-family:var(--mono);font-size:11.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--fg-muted);border-bottom:1px solid var(--border-l)}
55
+ td code{font-size:.82em}
56
+ tr:hover td{background:rgba(255,255,255,.014)}
57
+ .callout{border-left:3px solid var(--accent);background:var(--bg-light);border-radius:0 10px 10px 0;padding:14px 20px;margin:20px 0}
58
+ .callout.warn{border-left-color:var(--red)}
59
+ .callout.good{border-left-color:var(--green)}
60
+ .callout .t{font-family:var(--mono);font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--fg-muted);margin-bottom:4px}
61
+ pre{background:#16161e;border:1px solid var(--border);border-radius:11px;padding:16px 18px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:var(--fg-dim);margin:16px 0}
62
+ pre .k{color:var(--purple)} pre .s{color:var(--green)} pre .c{color:var(--fg-muted);font-style:italic} pre .f{color:var(--cyan)} pre .n{color:var(--orange)}
63
+ .toc{background:var(--bg-card);border:1px solid var(--border);border-radius:14px;padding:18px 24px;margin:30px 0;columns:2;column-gap:34px}
64
+ @media(max-width:760px){.toc{columns:1}}
65
+ .toc a{display:block;padding:4px 0;color:var(--fg-dim);font-size:14px;break-inside:avoid}
66
+ .toc a:hover{color:var(--accent);border:none}
67
+ .toc .num{font-family:var(--mono);color:var(--accent);font-size:12px;margin-right:8px}
68
+ figure{margin:24px 0;text-align:center}
69
+ figcaption{font-size:12.5px;color:var(--fg-muted);margin-top:10px;font-family:var(--mono)}
70
+ .flow{width:100%;height:auto;background:#16161e;border:1px solid var(--border);border-radius:12px}
71
+ .screenshot-sim{font-family:var(--mono);font-size:12px;background:#16161e;border:1px solid var(--border-l);border-radius:10px;padding:16px 18px;color:var(--fg-dim);line-height:1.7;margin:16px 0}
72
+ .screenshot-sim .q{color:var(--fg)} .screenshot-sim .opt{color:var(--cyan)} .screenshot-sim .cur{color:var(--yellow)}
73
+ .legend{font-family:var(--mono);font-size:12px;color:var(--fg-muted);display:flex;gap:20px;flex-wrap:wrap;margin:10px 0}
74
+ .dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px;vertical-align:middle}
75
+ .phase{border:1px solid var(--border);border-radius:13px;padding:4px 0;margin:16px 0;overflow:hidden}
76
+ .phase .head{display:flex;align-items:center;gap:12px;padding:14px 20px;background:var(--bg-light)}
77
+ .phase .head .badge{font-family:var(--mono);font-size:12px;font-weight:700;padding:3px 11px;border-radius:8px;background:var(--accent);color:#0b0f1e}
78
+ .phase .head h3{margin:0;font-size:16px}
79
+ .phase .head .eff{margin-left:auto;font-family:var(--mono);font-size:11.5px;color:var(--fg-muted)}
80
+ .phase .body{padding:8px 20px 16px}
81
+ .footer{margin-top:80px;padding-top:24px;border-top:1px solid var(--border);font-size:13px;color:var(--fg-muted)}
82
+ .src{font-size:13px;color:var(--fg-dim)}
83
+ .src li{margin:5px 0}
84
+ hr.soft{border:none;border-top:1px solid var(--border);margin:34px 0}
85
+ .tag{font-family:var(--mono);font-size:11px;color:var(--fg-muted)}
86
+ sup a{font-size:11px;border:none}
87
+ </style>
88
+ </head>
89
+ <body>
90
+
91
+ <header class="hero">
92
+ <p class="kicker">CTM · Internal Design Proposal</p>
93
+ <h1>A <span class="u">Unified Auto-Approver</span><br>across Claude, Codex &amp; beyond</h1>
94
+ <p class="sub">Why Codex approvals keep stalling on manual banners that should auto-approve — and a structured-protocol-first architecture that gives every agent the same fast, safe, learnable approval experience.</p>
95
+ <div class="meta">
96
+ <span><b>Status:</b> Draft for review</span>
97
+ <span><b>Scope:</b> Diagnosis + target architecture + roadmap</span>
98
+ <span><b>Code refs:</b> as of <code>main</code></span>
99
+ </div>
100
+ </header>
101
+
102
+ <div class="wrap">
103
+
104
+ <nav class="toc">
105
+ <a href="#summary"><span class="num">01</span>Executive summary</a>
106
+ <a href="#asis"><span class="num">02</span>How it works today</a>
107
+ <a href="#issues"><span class="num">03</span>What's broken &amp; why</a>
108
+ <a href="#survey"><span class="num">04</span>How others solve it</a>
109
+ <a href="#design"><span class="num">05</span>The unified design</a>
110
+ <a href="#roadmap"><span class="num">06</span>Phased roadmap</a>
111
+ <a href="#risks"><span class="num">07</span>Risks &amp; non-goals</a>
112
+ <a href="#appendix"><span class="num">08</span>Appendix &amp; sources</a>
113
+ </nav>
114
+
115
+ <!-- ============================ 01 ============================ -->
116
+ <h2 id="summary"><span class="n">01</span>Executive summary</h2>
117
+ <p class="lead">CTM already has the pieces of a great cross-agent auto-approver. The problem is that the <strong>robust pieces aren't the ones doing the work</strong>, and the fragile fallback path is what users actually hit — most visibly on Codex.</p>
118
+
119
+ <p>The triggering case: a read-only MCP call sat on a manual approval banner instead of auto-approving.</p>
120
+
121
+ <div class="screenshot-sim">
122
+ <span class="q">Allow the codebase-memory-mcp MCP server to run tool "search_graph"?</span><br>
123
+ &nbsp;&nbsp;limit: 20 · project: …ux-codex · query: OpenCode conversation import…<br><br>
124
+ <span class="cur">›</span> <span class="opt">1. Allow</span> &nbsp;&nbsp;<span style="color:var(--fg-muted)">Run the tool and continue.</span><br>
125
+ &nbsp;&nbsp;<span class="opt">2. Allow for this session</span><br>
126
+ &nbsp;&nbsp;<span class="opt">3. Always allow</span><br>
127
+ &nbsp;&nbsp;<span class="opt">4. Cancel</span><br><br>
128
+ <span style="color:var(--fg-muted)">enter to submit | esc to cancel</span>
129
+ </div>
130
+
131
+ <p><code>search_graph</code> is <em>read-only</em>. It should auto-approve instantly, the same way a Web Search does. Instead it blocks the agent and waits for a human. This is not a one-off — it is the predictable output of an architecture that:</p>
132
+
133
+ <div class="grid2">
134
+ <div class="card">
135
+ <h4 style="margin-top:0">The good news</h4>
136
+ <p style="margin-bottom:0">A <strong>single decision core</strong> (<code>decideApproval()</code>) and <strong>structured protocol adapters</strong> for both Claude (PreToolUse hook) and Codex (JSON-RPC approval bridge) <em>already exist in the tree</em>. The hard parts are largely built.</p>
137
+ </div>
138
+ <div class="card">
139
+ <h4 style="margin-top:0">The bad news</h4>
140
+ <p style="margin-bottom:0">Those structured paths are <strong>gated behind env flags</strong>. The <em>default</em> live path is <strong>terminal-scraping + simulated keystrokes</strong> across 11 provider parsers, backed by a <strong>fail-open LLM verifier that times out ~100% for Codex</strong>.</p>
141
+ </div>
142
+ </div>
143
+
144
+ <div class="callout">
145
+ <div class="t">The one-line thesis</div>
146
+ Promote the <strong>structured protocol channels</strong> (Claude PreToolUse hook, Codex JSON-RPC bridge, MCP-native) to be the <strong>default</strong>; route every channel through <strong>one policy engine</strong>; classify MCP risk from <strong>authoritative tool annotations</strong> instead of regex guesswork; and turn the LLM verifier from a <strong>universal fail-open fallback</strong> into a <strong>fast, posture-aware tie-breaker</strong> for the genuinely ambiguous middle.
147
+ </div>
148
+
149
+ <!-- ============================ 02 ============================ -->
150
+ <h2 id="asis"><span class="n">02</span>How it works today</h2>
151
+ <p>There are effectively <strong>two parallel approval systems</strong> in CTM. They share a decision core but reach it through very different front-ends.</p>
152
+
153
+ <figure>
154
+ <svg class="flow" viewBox="0 0 1000 420" xmlns="http://www.w3.org/2000/svg" font-family="var(--mono)">
155
+ <defs>
156
+ <marker id="ar" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#737aa2"/></marker>
157
+ </defs>
158
+ <!-- lanes -->
159
+ <text x="30" y="34" fill="#9ece6a" font-size="13" font-weight="700">PATH A · Structured (built, but gated)</text>
160
+ <text x="30" y="244" fill="#f7768e" font-size="13" font-weight="700">PATH B · Terminal-scrape + keystroke (the live default)</text>
161
+
162
+ <!-- Path A -->
163
+ <rect x="30" y="50" width="150" height="48" rx="9" fill="#1e2030" stroke="#3b4261"/><text x="105" y="72" fill="#c0caf5" font-size="12" text-anchor="middle">Claude PreToolUse</text><text x="105" y="88" fill="#737aa2" font-size="10.5" text-anchor="middle">HTTP hook (JSON)</text>
164
+ <rect x="30" y="110" width="150" height="48" rx="9" fill="#1e2030" stroke="#3b4261"/><text x="105" y="132" fill="#c0caf5" font-size="12" text-anchor="middle">Codex JSON-RPC</text><text x="105" y="148" fill="#737aa2" font-size="10.5" text-anchor="middle">requestApproval</text>
165
+ <line x1="180" y1="74" x2="360" y2="120" stroke="#737aa2" marker-end="url(#ar)"/>
166
+ <line x1="180" y1="134" x2="360" y2="128" stroke="#737aa2" marker-end="url(#ar)"/>
167
+
168
+ <!-- core -->
169
+ <rect x="365" y="96" width="270" height="64" rx="11" fill="#24283b" stroke="#7aa2f7" stroke-width="1.6"/>
170
+ <text x="500" y="124" fill="#7aa2f7" font-size="14" font-weight="700" text-anchor="middle">decideApproval()</text>
171
+ <text x="500" y="144" fill="#9aa5ce" font-size="10.5" text-anchor="middle">blocklist → rules → heuristic → verifier</text>
172
+
173
+ <!-- verdict -->
174
+ <rect x="700" y="96" width="160" height="64" rx="11" fill="#1e2030" stroke="#3b4261"/>
175
+ <text x="780" y="122" fill="#c0caf5" font-size="12" text-anchor="middle">Verdict</text>
176
+ <text x="780" y="140" fill="#737aa2" font-size="10" text-anchor="middle">allow / ask + risk</text>
177
+ <line x1="635" y1="128" x2="700" y2="128" stroke="#737aa2" marker-end="url(#ar)"/>
178
+
179
+ <!-- Path B -->
180
+ <rect x="30" y="266" width="150" height="46" rx="9" fill="#1e2030" stroke="#3b4261"/><text x="105" y="286" fill="#c0caf5" font-size="11.5" text-anchor="middle">PTY viewport</text><text x="105" y="302" fill="#737aa2" font-size="10" text-anchor="middle">scraped text</text>
181
+ <rect x="200" y="266" width="150" height="46" rx="9" fill="#1e2030" stroke="#3b4261"/><text x="275" y="286" fill="#c0caf5" font-size="11.5" text-anchor="middle">detectProvider()</text><text x="275" y="302" fill="#737aa2" font-size="10" text-anchor="middle">11 regex parsers</text>
182
+ <rect x="370" y="266" width="150" height="46" rx="9" fill="#1e2030" stroke="#3b4261"/><text x="445" y="286" fill="#c0caf5" font-size="11.5" text-anchor="middle">provider.parse()</text><text x="445" y="302" fill="#737aa2" font-size="10" text-anchor="middle">→ context</text>
183
+ <line x1="180" y1="289" x2="200" y2="289" stroke="#737aa2" marker-end="url(#ar)"/>
184
+ <line x1="350" y1="289" x2="370" y2="289" stroke="#737aa2" marker-end="url(#ar)"/>
185
+ <line x1="445" y1="266" x2="490" y2="160" stroke="#737aa2" marker-end="url(#ar)"/>
186
+ <text x="455" y="220" fill="#737aa2" font-size="10">via handleApprovalCheck()</text>
187
+
188
+ <!-- delivery -->
189
+ <rect x="700" y="266" width="160" height="46" rx="9" fill="#1e2030" stroke="#f7768e"/><text x="780" y="286" fill="#f7768e" font-size="11.5" text-anchor="middle">keystroke inject</text><text x="780" y="302" fill="#737aa2" font-size="10" text-anchor="middle">+150ms verify</text>
190
+ <line x1="780" y1="160" x2="780" y2="266" stroke="#737aa2" stroke-dasharray="4 3" marker-end="url(#ar)"/>
191
+ <text x="792" y="210" fill="#737aa2" font-size="10">deliver decision</text>
192
+
193
+ <!-- escalate -->
194
+ <rect x="700" y="350" width="160" height="44" rx="9" fill="#1e2030" stroke="#e0af68"/><text x="780" y="370" fill="#e0af68" font-size="11.5" text-anchor="middle">Pending tab</text><text x="780" y="386" fill="#737aa2" font-size="10" text-anchor="middle">escalation card</text>
195
+ <line x1="780" y1="160" x2="780" y2="350" stroke="#737aa2" stroke-dasharray="2 4" marker-end="url(#ar)"/>
196
+ </svg>
197
+ <figcaption>Two front-ends, one core. Path A is structured and robust but off-by-default; Path B is the brittle live default.</figcaption>
198
+ </figure>
199
+
200
+ <h3>The decision core (already shared)</h3>
201
+ <p><code>decideApproval(context, session)</code> in <code>approval-agent.js:2206</code> is a clean, layered cascade — and it is genuinely good:</p>
202
+ <ol>
203
+ <li><strong>Blocklist</strong> first, never overridden — <code>checkBlocklist()</code> (dangerous-command patterns: <code>rm -rf /</code>, force-push, <code>DROP TABLE</code>, exfil).</li>
204
+ <li><strong>Permission Manager rules</strong> — <code>matchPermission()</code> over the <code>perm_rules</code> table (user <code>allow</code>/<code>deny</code>, deny-with-exceptions).</li>
205
+ <li><strong>Learned signature rules</strong> — <code>findMatchingRule()</code> over <code>approval_rules</code> (normalized command signatures + regex fallback).</li>
206
+ <li><strong>Heuristic classification</strong> — <code>reviewWithHeuristics()</code> (read-only MCP, per-clause shell safety, network egress, high-risk patterns).</li>
207
+ <li><strong>Goal-aligned LLM verifier</strong> — <code>lib/auto-approval-verifier.js</code>, runs only for medium/high &amp; not-user-allowed.</li>
208
+ <li><strong>Fallback allow</strong>.</li>
209
+ </ol>
210
+ <p>It emits a normalized verdict: <code>{decision, decidedBy, riskLevel, reason, label, suggestedRule}</code>.</p>
211
+
212
+ <h3>Structured channels (built, gated)</h3>
213
+ <div class="grid2">
214
+ <div class="card">
215
+ <h4 style="margin-top:0">Claude · PreToolUse hook</h4>
216
+ <p class="tag">lib/approval-hook.js · <code>CTM_APPROVAL_HOOK=1</code></p>
217
+ <p style="margin-bottom:0">Claude calls an HTTP hook <em>before</em> running a tool, passing structured <code>{tool_name, tool_input}</code> — no scraping. <code>allow</code> suppresses the widget entirely; <code>ask</code> parks the request and raises a Pending card. Verified against Claude 2.1.173.</p>
218
+ </div>
219
+ <div class="card">
220
+ <h4 style="margin-top:0">Codex · JSON-RPC bridge</h4>
221
+ <p class="tag">lib/codex-approval-bridge.js</p>
222
+ <p style="margin-bottom:0">Recognizes Codex's native approval RPCs (<code>item/commandExecution/requestApproval</code>, <code>fileChange</code>, <code>permissions</code>) and maps the verdict back to <code>accept</code>/<code>decline</code>/<code>cancel</code>. Structured request, structured response.</p>
223
+ </div>
224
+ </div>
225
+
226
+ <h3>The live default (terminal-scrape + keystroke)</h3>
227
+ <p>For interactive sessions, the path that actually runs is <code>server.js</code> → <code>_handleApprovalDetection()</code> → <code>detectProvider()</code> (11 regex modules in <code>providers/</code>) → <code>provider.parse()</code> → <code>handleApprovalCheck()</code> → core → <code>_scheduleGuardedApproval()</code>, which <strong>types a simulated keystroke into the PTY</strong> and then watches for ~256 bytes of output within ~150 ms to confirm it "took" (else it sends backspaces to erase it). This is the path the screenshot is stuck in.</p>
228
+
229
+ <!-- ============================ 03 ============================ -->
230
+ <h2 id="issues"><span class="n">03</span>What's broken &amp; why</h2>
231
+ <p>Seven root causes, in rough order of how often they bite Codex users.</p>
232
+
233
+ <table>
234
+ <thead><tr><th style="width:26px">#</th><th>Root cause</th><th>Symptom</th><th>Evidence</th></tr></thead>
235
+ <tbody>
236
+ <tr>
237
+ <td><b>1</b></td>
238
+ <td><strong>Brittle scrape detection across 11 providers.</strong> Viewport wrap, ANSI, and footer drift break <code>detect</code>/<code>parse</code>; a misdetect sends the wrong keystroke.</td>
239
+ <td>Prompt not recognized → no auto-approve, or a stray <code>y</code> into the wrong widget.</td>
240
+ <td><code>providers/index.js</code>, <code>providers/codex.js</code>, <code>codex-mcp.js</code></td>
241
+ </tr>
242
+ <tr>
243
+ <td><b>2</b></td>
244
+ <td><strong>Structured-data loss on fallback.</strong> If the specific <code>codex-mcp</code> parser misses, generic <code>codex.js</code> emits <code>toolName='MCP'</code> with <em>no</em> <code>mcpTool</code> — the read verb vanishes.</td>
245
+ <td><code>search_graph</code> looks like an opaque "MCP op" → routed to the verifier.</td>
246
+ <td><code>providers/codex.js</code> (<code>.+ needs your approval</code>)</td>
247
+ </tr>
248
+ <tr>
249
+ <td><b>3</b></td>
250
+ <td><strong>MCP risk inferred by regex verb-guessing</strong> (snake_case boundary class) instead of authoritative MCP <em>tool annotations</em>.</td>
251
+ <td>Read tools mis-tiered; the boundary fix exists in code but is <em>not deployed</em> to the live primary, and is one parse-miss from failing anyway.</td>
252
+ <td><code>approval-agent.js</code> <code>readOnlyMcp</code>/<code>mutatingMcp</code></td>
253
+ </tr>
254
+ <tr>
255
+ <td><b>4</b></td>
256
+ <td><strong>The LLM verifier is the universal fallback, fail-OPEN, and ~100% times out for Codex</strong> (remote/slow vs a 20–25 s bound).</td>
257
+ <td>Medium+ ops either <em>stall on a manual banner</em> (when force-escalated) or get <em>approved blind</em> on timeout. Single point of failure.</td>
258
+ <td><code>lib/auto-approval-verifier.js</code>, <code>VERIFIER_OVERALL_TIMEOUT_MS</code></td>
259
+ </tr>
260
+ <tr>
261
+ <td><b>5</b></td>
262
+ <td><strong>Keystroke-injection races.</strong> The ~150 ms transition window is too short for slow Codex; plus user-driving backoff, stale-prompt revalidation, backspace erasure.</td>
263
+ <td>Approval typed but erased as a "false positive," or double-submitted. Decision is entangled with delivery timing.</td>
264
+ <td><code>sendApprovalKeystroke()</code>, <code>VERIFY_WINDOW_MS</code></td>
265
+ </tr>
266
+ <tr>
267
+ <td><b>6</b></td>
268
+ <td><strong>Per-symptom patch accretion.</strong> snake_case, network egress, MCP verb visibility, WebSearch, provider gate, compound-command judge — ~10 fixes layered onto one heuristic.</td>
269
+ <td>No single source of truth; several fixes <em>merged-but-not-deployed</em>, so the live primary lags the code.</td>
270
+ <td>git history of <code>approval-agent.js</code></td>
271
+ </tr>
272
+ <tr>
273
+ <td><b>7</b></td>
274
+ <td><strong>Fragmented surfacing.</strong> PTY, hook, and bridge paths record/raise escalations differently.</td>
275
+ <td>Risk of silent drop for backgrounded / fork prompts; inconsistent Pending behavior across agents.</td>
276
+ <td><code>_escalateUnresolvedApprovalToPending</code> vs <code>_parkHookApproval</code></td>
277
+ </tr>
278
+ </tbody>
279
+ </table>
280
+
281
+ <div class="callout warn">
282
+ <div class="t">Why the screenshot specifically hangs</div>
283
+ <code>search_graph</code> is read-only and the boundary-aware regex would tier it <span class="pill low">low</span> → auto-approve. But on the <em>live primary</em> the cause is a stack-up: the read classifier fix isn't deployed (#3, #6), and even deployed it depends on the verb surviving parse (#2). When it doesn't, the op falls to the verifier (#4) — which for Codex times out — so it lands on the manual banner you see.
284
+ </div>
285
+
286
+ <!-- ============================ 04 ============================ -->
287
+ <h2 id="survey"><span class="n">04</span>How others solve it</h2>
288
+ <p>The mature pattern across the ecosystem is remarkably consistent: <strong>a deterministic allowlist/denylist for the obvious 95%, a small classifier for the ambiguous 5%, and risk expressed as metadata rather than guessed from strings.</strong></p>
289
+
290
+ <table>
291
+ <thead><tr><th>System</th><th>Model</th><th>Lesson for CTM</th></tr></thead>
292
+ <tbody>
293
+ <tr><td><strong>Claude Code</strong> <sup><a href="#s1">[1]</a></sup></td><td><code>deny → ask → allow</code>, first match wins; deny can't be overridden. <strong>PreToolUse hooks</strong> run before the prompt (exit-2 = hard block).</td><td>Hooks are the structured decision point. Precedence order is a clean mental model. CTM already implements this hook.</td></tr>
294
+ <tr><td><strong>OpenAI Codex</strong> <sup><a href="#s2">[2]</a></sup></td><td>Two orthogonal axes: <code>approval_policy</code> (untrusted / on-request / never / granular) × <code>sandbox_mode</code> (read-only / workspace-write / danger-full-access).</td><td>Separate <em>"is it allowed"</em> from <em>"how contained is it."</em> Sandbox + approval are complementary, not competing.</td></tr>
295
+ <tr><td><strong>Cursor</strong> <sup><a href="#s3">[3]</a></sup></td><td>Three-stage filter: <strong>allowlist</strong> runs the ~80% obvious-safe instantly, <strong>sandbox</strong> contains the ~15% risky-but-containable, <strong>classifier subagent</strong> judges the ~5% that need it.</td><td>The template for CTM's tiering. The LLM should see only the 5%, never the 80%.</td></tr>
296
+ <tr><td><strong>Cline</strong> <sup><a href="#s4">[4]</a></sup></td><td>Model flags each call <code>requires_approval</code>; a <strong>hard denylist</strong> (<code>rm -rf</code>, <code>DROP TABLE</code>, force-push) always prompts regardless of auto-approve.</td><td>A fail-closed denylist is non-negotiable, even in "auto" mode — the prompt-injection backstop.</td></tr>
297
+ <tr><td><strong>MCP tool annotations</strong> <sup><a href="#s5">[5]</a></sup></td><td><code>readOnlyHint</code>, <code>destructiveHint</code>, <code>idempotentHint</code>, <code>openWorldHint</code> — servers <em>declare</em> tool risk; defaults assume worst case.</td><td>Stop guessing read-vs-write from the tool <em>name</em>. Ask the server. (Treat as advisory: fast-path reads, still gate destructive.)</td></tr>
298
+ <tr><td><strong>Least-privilege / capability containment</strong> <sup><a href="#s6">[6]</a></sup></td><td>Risk tier as tool metadata: low (read/reversible), medium (bounded write), high (destructive/bulk/egress). Scope down, don't just block.</td><td>Risk is a property of the <em>tool</em>, registered once — not re-derived per call by regex.</td></tr>
299
+ </tbody>
300
+ </table>
301
+
302
+ <!-- ============================ 05 ============================ -->
303
+ <h2 id="design"><span class="n">05</span>The unified design</h2>
304
+ <p class="lead">North star: <strong>structured-protocol-first, scrape-as-fallback; one policy core; authoritative metadata; a fast posture-aware verifier; one UX.</strong></p>
305
+
306
+ <figure>
307
+ <svg class="flow" viewBox="0 0 1000 360" xmlns="http://www.w3.org/2000/svg">
308
+ <defs><marker id="ar2" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#737aa2"/></marker></defs>
309
+ <!-- adapters -->
310
+ <text x="30" y="28" fill="#9aa5ce" font-size="12" font-weight="700">CHANNEL ADAPTERS → one ToolCall</text>
311
+ <rect x="30" y="40" width="150" height="38" rx="8" fill="#1e2030" stroke="#9ece6a"/><text x="105" y="64" fill="#9ece6a" font-size="11" text-anchor="middle">Claude hook</text>
312
+ <rect x="30" y="86" width="150" height="38" rx="8" fill="#1e2030" stroke="#9ece6a"/><text x="105" y="110" fill="#9ece6a" font-size="11" text-anchor="middle">Codex JSON-RPC</text>
313
+ <rect x="30" y="132" width="150" height="38" rx="8" fill="#1e2030" stroke="#7dcfff"/><text x="105" y="156" fill="#7dcfff" font-size="11" text-anchor="middle">MCP-native</text>
314
+ <rect x="30" y="178" width="150" height="38" rx="8" fill="#1e2030" stroke="#e0af68"/><text x="105" y="196" fill="#e0af68" font-size="10.5" text-anchor="middle">PTY-scrape (fallback)</text>
315
+
316
+ <!-- engine -->
317
+ <rect x="250" y="70" width="230" height="120" rx="12" fill="#24283b" stroke="#7aa2f7" stroke-width="1.6"/>
318
+ <text x="365" y="96" fill="#7aa2f7" font-size="13" font-weight="700" text-anchor="middle">Policy Engine</text>
319
+ <rect x="268" y="108" width="194" height="22" rx="6" fill="#1a1b26" stroke="#9ece6a"/><text x="365" y="123" fill="#9ece6a" font-size="10.5" text-anchor="middle">T1 Allowlist · instant</text>
320
+ <rect x="268" y="134" width="194" height="22" rx="6" fill="#1a1b26" stroke="#f7768e"/><text x="365" y="149" fill="#f7768e" font-size="10.5" text-anchor="middle">T2 Denylist · fail-closed</text>
321
+ <rect x="268" y="160" width="194" height="22" rx="6" fill="#1a1b26" stroke="#e0af68"/><text x="365" y="175" fill="#e0af68" font-size="10.5" text-anchor="middle">T3 Classifier · the ~5%</text>
322
+ <line x1="180" y1="105" x2="250" y2="120" stroke="#737aa2" marker-end="url(#ar2)"/>
323
+ <line x1="180" y1="151" x2="250" y2="140" stroke="#737aa2" marker-end="url(#ar2)"/>
324
+
325
+ <!-- metadata feed -->
326
+ <rect x="250" y="220" width="230" height="40" rx="9" fill="#1e2030" stroke="#bb9af7"/><text x="365" y="237" fill="#bb9af7" font-size="11" text-anchor="middle">Risk Registry</text><text x="365" y="252" fill="#737aa2" font-size="9.5" text-anchor="middle">MCP annotations · per-server tiers</text>
327
+ <line x1="365" y1="220" x2="365" y2="190" stroke="#737aa2" stroke-dasharray="3 3" marker-end="url(#ar2)"/>
328
+
329
+ <!-- verdict + delivery -->
330
+ <rect x="540" y="110" width="150" height="44" rx="10" fill="#1e2030" stroke="#3b4261"/><text x="615" y="130" fill="#c0caf5" font-size="12" text-anchor="middle">Verdict</text><text x="615" y="146" fill="#737aa2" font-size="9.5" text-anchor="middle">allow / ask + reason</text>
331
+ <line x1="480" y1="130" x2="540" y2="130" stroke="#737aa2" marker-end="url(#ar2)"/>
332
+
333
+ <text x="760" y="60" fill="#9aa5ce" font-size="12" font-weight="700">DELIVERY (decoupled)</text>
334
+ <rect x="760" y="74" width="210" height="34" rx="8" fill="#1e2030" stroke="#3b4261"/><text x="865" y="96" fill="#c0caf5" font-size="11" text-anchor="middle">hook response / RPC ack</text>
335
+ <rect x="760" y="116" width="210" height="34" rx="8" fill="#1e2030" stroke="#3b4261"/><text x="865" y="138" fill="#c0caf5" font-size="11" text-anchor="middle">keystroke (fallback only)</text>
336
+ <rect x="760" y="158" width="210" height="34" rx="8" fill="#1e2030" stroke="#e0af68"/><text x="865" y="180" fill="#e0af68" font-size="11" text-anchor="middle">Pending card (on ask)</text>
337
+ <line x1="690" y1="125" x2="760" y2="100" stroke="#737aa2" marker-end="url(#ar2)"/>
338
+ <line x1="690" y1="132" x2="760" y2="133" stroke="#737aa2" marker-end="url(#ar2)"/>
339
+ <line x1="690" y1="140" x2="760" y2="172" stroke="#737aa2" marker-end="url(#ar2)"/>
340
+ </svg>
341
+ <figcaption>Every channel normalizes to one <code>ToolCall</code> → one tiered Policy Engine → one <code>Verdict</code> → channel-appropriate delivery. The LLM only ever sees Tier 3.</figcaption>
342
+ </figure>
343
+
344
+ <h3>The seven principles</h3>
345
+
346
+ <h4>P-A · One policy engine, one verdict</h4>
347
+ <p>Route <em>every</em> channel through <code>decideApproval()</code>, and make the live PTY path (<code>handleApprovalCheck</code>) consume the <strong>full verdict</strong> — its <code>decision</code>, not just <code>riskLevel</code>. Today the PTY path re-derives its own decision from the risk level, which is how a heuristic that says "escalate" can still get approved. One input schema, one output schema, one set of tests.</p>
348
+
349
+ <h4>P-B · Promote structured channels to default</h4>
350
+ <p>Flip the defaults. The Claude PreToolUse hook and the Codex JSON-RPC bridge become the <em>primary</em> channel wherever the CLI supports them; prefer MCP-native elicitation where available. <strong>Terminal-scrape is demoted to a normalizing adapter</strong> that emits the same <code>ToolCall</code> — used only when no structured channel exists. This deletes whole classes of bug (#1, #2, #5) for the agents that matter most.</p>
351
+
352
+ <h4>P-C · Tiered decision (Cursor's 3-stage)</h4>
353
+ <div class="legend">
354
+ <span><span class="dot" style="background:var(--green)"></span>T1 Allowlist — instant, deterministic</span>
355
+ <span><span class="dot" style="background:var(--red)"></span>T2 Denylist — instant, fail-closed</span>
356
+ <span><span class="dot" style="background:var(--yellow)"></span>T3 Classifier — the ~5%</span>
357
+ </div>
358
+ <ul>
359
+ <li><span class="pill low">T1</span> user <code>perm_rules</code> + learned signatures + read-only metadata + known-safe verbs → <strong>auto-allow, no model call</strong>.</li>
360
+ <li><span class="pill high">T2</span> blocklist + egress-off-allowlist + destructive patterns → <strong>always escalate</strong>, never auto-approved, even in "auto" mode.</li>
361
+ <li><span class="pill med">T3</span> only the genuinely ambiguous middle reaches the classifier.</li>
362
+ </ul>
363
+
364
+ <h4>P-D · Authoritative risk metadata over regex</h4>
365
+ <p>Build a <strong>per-server Risk Registry</strong> fed by MCP tool annotations (<code>readOnlyHint</code>, <code>destructiveHint</code>, <code>openWorldHint</code>) pulled from each server's capability list. <code>search_graph</code> would arrive pre-labeled read-only — no boundary regex, no verb-guessing, no parse-survival dependency. Fall back to the verb heuristic only when a server omits annotations. Because hints are advisory and spoofable, they fast-path <em>reads</em> but never auto-clear <em>destructive</em>.</p>
366
+
367
+ <h4>P-E · Fix the verifier</h4>
368
+ <p>Make the verifier a <strong>fast, local tie-breaker</strong>, not a universal fallback:</p>
369
+ <ul>
370
+ <li>Dedicated low-latency model (e.g. a Haiku subprocess or local model) with a tight timeout, so Codex sessions don't blow the 20–25 s bound.</li>
371
+ <li><strong>Explicit posture per risk class</strong>: fail-<em>open</em> for local-dev/reversible, fail-<em>closed</em> for egress / external / destructive. (Today everything fails open — uncertain becomes "approve.")</li>
372
+ <li>It only ever runs on Tier 3, so timeouts can't strand the obvious-safe majority.</li>
373
+ </ul>
374
+
375
+ <h4>P-F · Decouple decision from delivery</h4>
376
+ <p>Isolate "deliver the decision" (keystroke / hook response / RPC ack / Pending card) behind one interface, with <strong>per-provider timing</strong>. The policy engine becomes a pure, synchronous, unit-testable function; the flaky timing logic lives in one place and Codex gets a longer verify window than Claude.</p>
377
+
378
+ <h4>P-G · Unified UX</h4>
379
+ <p>One Pending tab and one rules manager for all agents. Generalize the rule vocabulary with a first-class MCP form — <code>MCP(server/verb)</code> and <code>MCP(server:readonly)</code> — alongside <code>Bash(cmd:*)</code> and <code>Network(host)</code>. One-click "approve all like this" mints a uniform, generalized rule; per-session and per-project <strong>posture controls</strong> (e.g. <em>Cautious / Balanced / Autonomous</em>) set the default tiering without code changes.</p>
380
+
381
+ <h3>The normalized schema</h3>
382
+ <pre><span class="c">// One shape, regardless of which agent or channel produced it</span>
383
+ <span class="k">type</span> <span class="f">ToolCall</span> = {
384
+ provider: <span class="s">'claude'</span> | <span class="s">'codex'</span> | <span class="s">'cursor'</span> | …,
385
+ channel: <span class="s">'hook'</span> | <span class="s">'rpc'</span> | <span class="s">'mcp'</span> | <span class="s">'scrape'</span>, <span class="c">// provenance, for delivery</span>
386
+ sessionId: <span class="k">string</span>,
387
+ tool: <span class="k">string</span>, <span class="c">// 'Bash' | 'Edit' | 'MCP' | 'Network' | …</span>
388
+ verb: <span class="k">string</span>|<span class="k">null</span>, <span class="c">// 'search_graph' (survives fallback)</span>
389
+ server: <span class="k">string</span>|<span class="k">null</span>, <span class="c">// 'codebase-memory-mcp'</span>
390
+ target: <span class="k">string</span>, <span class="c">// command / file / host</span>
391
+ args: <span class="k">object</span>, <span class="c">// structured when available</span>
392
+ riskHints: { readOnly?: <span class="k">bool</span>, destructive?: <span class="k">bool</span>, openWorld?: <span class="k">bool</span> } <span class="c">// from Registry</span>
393
+ }
394
+
395
+ <span class="k">type</span> <span class="f">Verdict</span> = {
396
+ decision: <span class="s">'allow'</span> | <span class="s">'ask'</span> | <span class="s">'deny'</span>,
397
+ tier: <span class="n">1</span> | <span class="n">2</span> | <span class="n">3</span>,
398
+ decidedBy: <span class="s">'rule'</span> | <span class="s">'metadata'</span> | <span class="s">'blocklist'</span> | <span class="s">'verifier'</span> | …,
399
+ riskLevel: <span class="s">'low'</span> | <span class="s">'medium'</span> | <span class="s">'high'</span>,
400
+ reason: <span class="k">string</span>,
401
+ suggestedRule: <span class="k">string</span>|<span class="k">null</span> <span class="c">// 'MCP(codebase-memory-mcp:readonly)'</span>
402
+ }</pre>
403
+
404
+ <!-- ============================ 06 ============================ -->
405
+ <h2 id="roadmap"><span class="n">06</span>Phased roadmap</h2>
406
+ <p>Ordered so the highest-relief, lowest-risk work ships first. Phases 1–2 are days; 3–5 are the structural investment.</p>
407
+
408
+ <div class="phase">
409
+ <div class="head"><span class="badge">P1</span><h3>Stop the bleeding</h3><span class="eff">quick wins · low risk</span></div>
410
+ <div class="body">
411
+ <ul>
412
+ <li><strong>Deploy the merged-but-not-deployed approver fixes</strong> to the live primary (the read classifier, MCP verb visibility, network egress). This alone fixes the screenshot.</li>
413
+ <li><strong>Make the verb survive fallback</strong>: teach generic <code>codex.js</code> to extract <code>{server, verb}</code> from the "Allow the X server to run tool Y" shape (duplicate the <code>codex-mcp.js</code> logic) so a parser miss never hides a read.</li>
414
+ <li><strong>Extend the Codex keystroke-verify window</strong> / make it provider-specific so slow sessions stop erasing valid approvals.</li>
415
+ </ul>
416
+ </div>
417
+ </div>
418
+
419
+ <div class="phase">
420
+ <div class="head"><span class="badge">P2</span><h3>One verdict, honestly consumed</h3><span class="eff">core unification</span></div>
421
+ <div class="body">
422
+ <ul>
423
+ <li>Make <code>handleApprovalCheck</code> consume the <strong>full verdict</strong> from <code>decideApproval()</code> (P-A) — kill the divergent risk-only re-derivation.</li>
424
+ <li>Introduce the normalized <code>ToolCall</code>/<code>Verdict</code> schema and route both PTY and hook/bridge through it.</li>
425
+ <li><strong>Verifier posture split</strong> (P-E, partial): fail-closed for egress/external/destructive; keep fail-open for local-dev. Tighten the timeout.</li>
426
+ </ul>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="phase">
431
+ <div class="head"><span class="badge">P3</span><h3>Structured-first</h3><span class="eff">flip the defaults</span></div>
432
+ <div class="body">
433
+ <ul>
434
+ <li>Turn the <strong>Claude PreToolUse hook on by default</strong>; make the <strong>Codex JSON-RPC bridge primary</strong> for proto-capable sessions (P-B).</li>
435
+ <li>Demote terminal-scrape to a normalizing adapter behind the same schema.</li>
436
+ <li>Decouple delivery (P-F): one interface, per-provider timing; the engine becomes pure + unit-tested.</li>
437
+ </ul>
438
+ </div>
439
+ </div>
440
+
441
+ <div class="phase">
442
+ <div class="head"><span class="badge">P4</span><h3>Metadata over guesswork</h3><span class="eff">risk registry</span></div>
443
+ <div class="body">
444
+ <ul>
445
+ <li>Build the <strong>per-server Risk Registry</strong> from MCP tool annotations (P-D); read tools fast-path with zero model calls and zero regex.</li>
446
+ <li>Formalize the three tiers (P-C) with the denylist as an explicit fail-closed gate.</li>
447
+ </ul>
448
+ </div>
449
+ </div>
450
+
451
+ <div class="phase">
452
+ <div class="head"><span class="badge">P5</span><h3>One experience</h3><span class="eff">unified UX</span></div>
453
+ <div class="body">
454
+ <ul>
455
+ <li>Unify the Pending tab + rules manager across all agents; add the <code>MCP(server/verb)</code> rule form (P-G).</li>
456
+ <li>Per-session / per-project <strong>posture controls</strong> (Cautious / Balanced / Autonomous).</li>
457
+ <li>One-click "approve all like this" minting uniform generalized rules.</li>
458
+ </ul>
459
+ </div>
460
+ </div>
461
+
462
+ <!-- ============================ 07 ============================ -->
463
+ <h2 id="risks"><span class="n">07</span>Risks &amp; non-goals</h2>
464
+ <div class="grid2">
465
+ <div class="card">
466
+ <h4 style="margin-top:0">Risks to manage</h4>
467
+ <ul style="margin-bottom:0">
468
+ <li><strong>Annotation spoofing</strong> — a malicious/buggy server could claim <code>readOnlyHint</code>. Mitigation: hints fast-path reads only; destructive/egress always gated regardless of hints.</li>
469
+ <li><strong>Prompt injection in "auto" mode</strong> — the fail-closed denylist (T2) must stay non-negotiable even at the most autonomous posture.</li>
470
+ <li><strong>Hook/CLI version drift</strong> — structured channels depend on CLI behavior; keep scrape as a tested fallback and pin verified versions.</li>
471
+ <li><strong>Migration safety</strong> — ship behind flags, dev-instance first, never test against the live primary.</li>
472
+ </ul>
473
+ </div>
474
+ <div class="card">
475
+ <h4 style="margin-top:0">Explicit non-goals</h4>
476
+ <ul style="margin-bottom:0">
477
+ <li>Replacing OS-level sandboxing — approval and containment stay complementary axes (the Codex lesson).</li>
478
+ <li>A new permissions <em>storage</em> model — <code>perm_rules</code> / <code>approval_rules</code> stay; we generalize the vocabulary, not the tables.</li>
479
+ <li>Auto-approving genuinely destructive or external actions without a human — never the goal.</li>
480
+ <li>This document is design only; no code changes ship with it.</li>
481
+ </ul>
482
+ </div>
483
+ </div>
484
+
485
+ <!-- ============================ 08 ============================ -->
486
+ <h2 id="appendix"><span class="n">08</span>Appendix &amp; sources</h2>
487
+
488
+ <h3>Key files &amp; functions</h3>
489
+ <table>
490
+ <thead><tr><th>Concern</th><th>Where</th></tr></thead>
491
+ <tbody>
492
+ <tr><td>Shared decision core</td><td><code>approval-agent.js</code> · <code>decideApproval()</code>, <code>reviewWithHeuristics()</code>, <code>findMatchingRule()</code>, <code>normalizeCommandSignature()</code></td></tr>
493
+ <tr><td>Live PTY cascade</td><td><code>server.js</code> · <code>_handleApprovalDetection()</code> → <code>handleApprovalCheck()</code> → <code>_scheduleGuardedApproval()</code> / <code>sendApprovalKeystroke()</code></td></tr>
494
+ <tr><td>Provider parsers (×11)</td><td><code>providers/index.js</code>, <code>providers/codex.js</code>, <code>providers/codex-mcp.js</code>, <code>providers/claude-code.js</code>, …</td></tr>
495
+ <tr><td>Verifier</td><td><code>lib/auto-approval-verifier.js</code> · <code>verifyIfEnabled()</code>, <code>VERIFIER_OVERALL_TIMEOUT_MS</code></td></tr>
496
+ <tr><td>Claude hook adapter</td><td><code>lib/approval-hook.js</code> · <code>buildClaudeHookSettings()</code>, <code>contextFromHookPayload()</code>, <code>mapDecisionToHookOutput()</code>, <code>createHookApprovalPark()</code></td></tr>
497
+ <tr><td>Codex bridge adapter</td><td><code>lib/codex-approval-bridge.js</code> · <code>parseCodexApprovalRequest()</code>, <code>buildCodexApprovalResponse()</code></td></tr>
498
+ <tr><td>Permission matching</td><td><code>lib/permission-match.js</code> · <code>matchPermission()</code>; tables <code>perm_rules</code>, <code>approval_rules</code></td></tr>
499
+ <tr><td>Escalation → Pending</td><td><code>lib/escalation-review.js</code> · <code>groupEscalations()</code>, <code>suggestAllowRule()</code>; <code>approval_decisions</code> table</td></tr>
500
+ <tr><td>UI surfaces</td><td><code>public/index.html</code> · Pending/Rules/History/Blocklist tabs, session pending banner</td></tr>
501
+ </tbody>
502
+ </table>
503
+
504
+ <h3 id="sources">Sources</h3>
505
+ <ol class="src">
506
+ <li id="s1"><a href="https://code.claude.com/docs/en/permissions">Configure permissions — Claude Code Docs</a> · <a href="https://platform.claude.com/docs/en/agent-sdk/permissions">Agent SDK permissions</a></li>
507
+ <li id="s2"><a href="https://developers.openai.com/codex/agent-approvals-security">Agent approvals &amp; security — Codex</a> · <a href="https://developers.openai.com/codex/concepts/sandboxing">Sandbox — Codex</a> · <a href="https://developers.openai.com/codex/config-reference">Config reference</a></li>
508
+ <li id="s3"><a href="https://www.totalum.app/blog/cursor-auto-review-totalum">Cursor Auto-review Run Mode (3-stage filter)</a></li>
509
+ <li id="s4"><a href="https://docs.cline.bot/features/auto-approve">Auto Approve &amp; YOLO Mode — Cline</a> · <a href="https://www.theregister.com/2025/07/21/cursor_ai_safeguards_easily_bypassed/">YOLO-mode injection risk (The Register)</a></li>
510
+ <li id="s5"><a href="https://blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/">Tool Annotations as Risk Vocabulary — MCP Blog</a> · <a href="https://sunpeak.ai/blogs/testing-mcp-tool-annotations/">Testing MCP tool annotations</a></li>
511
+ <li id="s6"><a href="https://notes.muthu.co/2026/04/least-privilege-and-capability-containment-designing-agents-that-cannot-exceed-their-mandate/">Least Privilege &amp; Capability Containment</a> · <a href="https://www.osohq.com/learn/how-to-prevent-over-permissioned-agents">Preventing over-permissioned agents (Oso)</a></li>
512
+ </ol>
513
+
514
+ <div class="footer">
515
+ <p>CTM internal design proposal · Unified Auto-Approver. Architecture claims cross-checked against the current tree; see the file/function map above. This is a design artifact — no code changes accompany it.</p>
516
+ </div>
517
+
518
+ </div>
519
+ </body>
520
+ </html>