agent-composer 0.3.1 → 0.4.1

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 (180) hide show
  1. package/README.md +495 -180
  2. package/composer.config.schema.json +206 -2
  3. package/dist/cli/cleanup.d.ts +24 -0
  4. package/dist/cli/cleanup.js +151 -0
  5. package/dist/cli/cleanup.js.map +1 -0
  6. package/dist/cli/doctor.d.ts +12 -0
  7. package/dist/cli/doctor.js +244 -4
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/cli/goal.d.ts +28 -0
  10. package/dist/cli/goal.js +251 -0
  11. package/dist/cli/goal.js.map +1 -0
  12. package/dist/cli/help.d.ts +3 -0
  13. package/dist/cli/help.js +31 -0
  14. package/dist/cli/help.js.map +1 -0
  15. package/dist/cli/init.d.ts +5 -0
  16. package/dist/cli/init.js +116 -21
  17. package/dist/cli/init.js.map +1 -1
  18. package/dist/cli/initArgs.d.ts +16 -0
  19. package/dist/cli/initArgs.js +19 -0
  20. package/dist/cli/initArgs.js.map +1 -0
  21. package/dist/cli/installGitHook.d.ts +7 -0
  22. package/dist/cli/installGitHook.js +61 -0
  23. package/dist/cli/installGitHook.js.map +1 -0
  24. package/dist/cli/mode.d.ts +6 -0
  25. package/dist/cli/mode.js +25 -0
  26. package/dist/cli/mode.js.map +1 -0
  27. package/dist/cli/status.d.ts +105 -0
  28. package/dist/cli/status.js +400 -0
  29. package/dist/cli/status.js.map +1 -0
  30. package/dist/config/env.d.ts +1 -1
  31. package/dist/config/modes.d.ts +10 -0
  32. package/dist/config/modes.js +26 -0
  33. package/dist/config/modes.js.map +1 -0
  34. package/dist/config/oracleRole.d.ts +10 -0
  35. package/dist/config/oracleRole.js +11 -0
  36. package/dist/config/oracleRole.js.map +1 -0
  37. package/dist/config/schema.d.ts +246 -0
  38. package/dist/config/schema.js +127 -2
  39. package/dist/config/schema.js.map +1 -1
  40. package/dist/evolve/reflection.d.ts +9 -0
  41. package/dist/evolve/reflection.js +14 -0
  42. package/dist/evolve/reflection.js.map +1 -1
  43. package/dist/evolve/runner.d.ts +2 -1
  44. package/dist/evolve/runner.js +2 -1
  45. package/dist/evolve/runner.js.map +1 -1
  46. package/dist/index.js +115 -6
  47. package/dist/index.js.map +1 -1
  48. package/dist/providers/AnthropicCompatibleProvider.d.ts +13 -1
  49. package/dist/providers/AnthropicCompatibleProvider.js +115 -9
  50. package/dist/providers/AnthropicCompatibleProvider.js.map +1 -1
  51. package/dist/providers/CLIProvider.d.ts +18 -0
  52. package/dist/providers/CLIProvider.js +265 -62
  53. package/dist/providers/CLIProvider.js.map +1 -1
  54. package/dist/providers/IProvider.d.ts +12 -0
  55. package/dist/providers/SpendGuardProvider.d.ts +32 -0
  56. package/dist/providers/SpendGuardProvider.js +98 -0
  57. package/dist/providers/SpendGuardProvider.js.map +1 -0
  58. package/dist/registry.d.ts +5 -2
  59. package/dist/registry.js +17 -2
  60. package/dist/registry.js.map +1 -1
  61. package/dist/server/activeRuns.d.ts +17 -0
  62. package/dist/server/activeRuns.js +114 -0
  63. package/dist/server/activeRuns.js.map +1 -0
  64. package/dist/server/codexLifecycleRunner.d.ts +29 -0
  65. package/dist/server/codexLifecycleRunner.js +188 -0
  66. package/dist/server/codexLifecycleRunner.js.map +1 -0
  67. package/dist/server/configMutation.d.ts +22 -0
  68. package/dist/server/configMutation.js +121 -0
  69. package/dist/server/configMutation.js.map +1 -0
  70. package/dist/server/handoffContext.d.ts +1 -0
  71. package/dist/server/handoffContext.js +12 -0
  72. package/dist/server/handoffContext.js.map +1 -0
  73. package/dist/server/progress.d.ts +24 -0
  74. package/dist/server/progress.js +109 -0
  75. package/dist/server/progress.js.map +1 -0
  76. package/dist/server/toolDescriptions.d.ts +60 -0
  77. package/dist/server/toolDescriptions.js +134 -0
  78. package/dist/server/toolDescriptions.js.map +1 -0
  79. package/dist/server.d.ts +19 -25
  80. package/dist/server.js +87 -377
  81. package/dist/server.js.map +1 -1
  82. package/dist/tools/audit.d.ts +2 -0
  83. package/dist/tools/audit.js +66 -0
  84. package/dist/tools/audit.js.map +1 -0
  85. package/dist/tools/code.d.ts +2 -0
  86. package/dist/tools/code.js +160 -0
  87. package/dist/tools/code.js.map +1 -0
  88. package/dist/tools/codexLifecycle.d.ts +2 -0
  89. package/dist/tools/codexLifecycle.js +206 -0
  90. package/dist/tools/codexLifecycle.js.map +1 -0
  91. package/dist/tools/config.d.ts +2 -0
  92. package/dist/tools/config.js +183 -0
  93. package/dist/tools/config.js.map +1 -0
  94. package/dist/tools/context.d.ts +31 -0
  95. package/dist/tools/context.js +2 -0
  96. package/dist/tools/context.js.map +1 -0
  97. package/dist/tools/goal.d.ts +2 -0
  98. package/dist/tools/goal.js +159 -0
  99. package/dist/tools/goal.js.map +1 -0
  100. package/dist/tools/handoff.d.ts +2 -0
  101. package/dist/tools/handoff.js +57 -0
  102. package/dist/tools/handoff.js.map +1 -0
  103. package/dist/tools/oracle.d.ts +2 -0
  104. package/dist/tools/oracle.js +248 -0
  105. package/dist/tools/oracle.js.map +1 -0
  106. package/dist/tools/research.d.ts +2 -0
  107. package/dist/tools/research.js +51 -0
  108. package/dist/tools/research.js.map +1 -0
  109. package/dist/tools/review.d.ts +2 -0
  110. package/dist/tools/review.js +233 -0
  111. package/dist/tools/review.js.map +1 -0
  112. package/dist/tools/route.d.ts +2 -0
  113. package/dist/tools/route.js +69 -0
  114. package/dist/tools/route.js.map +1 -0
  115. package/dist/tools/session.d.ts +2 -0
  116. package/dist/tools/session.js +37 -0
  117. package/dist/tools/session.js.map +1 -0
  118. package/dist/tools/status.d.ts +2 -0
  119. package/dist/tools/status.js +34 -0
  120. package/dist/tools/status.js.map +1 -0
  121. package/dist/tools/workflow.d.ts +2 -0
  122. package/dist/tools/workflow.js +27 -0
  123. package/dist/tools/workflow.js.map +1 -0
  124. package/dist/util/applyFileBlocks.d.ts +18 -0
  125. package/dist/util/applyFileBlocks.js +163 -0
  126. package/dist/util/applyFileBlocks.js.map +1 -0
  127. package/dist/util/asyncControl.d.ts +14 -0
  128. package/dist/util/asyncControl.js +106 -0
  129. package/dist/util/asyncControl.js.map +1 -0
  130. package/dist/util/auditLog.d.ts +56 -0
  131. package/dist/util/auditLog.js +232 -0
  132. package/dist/util/auditLog.js.map +1 -0
  133. package/dist/util/codexLifecycle.d.ts +55 -0
  134. package/dist/util/codexLifecycle.js +102 -0
  135. package/dist/util/codexLifecycle.js.map +1 -0
  136. package/dist/util/codexLifecycleJob.d.ts +209 -0
  137. package/dist/util/codexLifecycleJob.js +360 -0
  138. package/dist/util/codexLifecycleJob.js.map +1 -0
  139. package/dist/util/composerDisabled.d.ts +6 -0
  140. package/dist/util/composerDisabled.js +27 -0
  141. package/dist/util/composerDisabled.js.map +1 -0
  142. package/dist/util/dispatchHint.d.ts +5 -3
  143. package/dist/util/dispatchHint.js +62 -2
  144. package/dist/util/dispatchHint.js.map +1 -1
  145. package/dist/util/goal.d.ts +132 -0
  146. package/dist/util/goal.js +616 -0
  147. package/dist/util/goal.js.map +1 -0
  148. package/dist/util/goalReport.d.ts +51 -0
  149. package/dist/util/goalReport.js +164 -0
  150. package/dist/util/goalReport.js.map +1 -0
  151. package/dist/util/jobPolling.d.ts +9 -0
  152. package/dist/util/jobPolling.js +17 -0
  153. package/dist/util/jobPolling.js.map +1 -0
  154. package/dist/util/oracleJob.d.ts +66 -0
  155. package/dist/util/oracleJob.js +295 -0
  156. package/dist/util/oracleJob.js.map +1 -0
  157. package/dist/util/oracleLock.d.ts +38 -0
  158. package/dist/util/oracleLock.js +182 -0
  159. package/dist/util/oracleLock.js.map +1 -0
  160. package/dist/util/reviewDiff.d.ts +8 -0
  161. package/dist/util/reviewDiff.js +29 -0
  162. package/dist/util/reviewDiff.js.map +1 -0
  163. package/dist/util/reviewJob.d.ts +57 -0
  164. package/dist/util/reviewJob.js +207 -0
  165. package/dist/util/reviewJob.js.map +1 -0
  166. package/dist/util/workflowPlan.d.ts +24 -0
  167. package/dist/util/workflowPlan.js +49 -0
  168. package/dist/util/workflowPlan.js.map +1 -0
  169. package/package.json +8 -1
  170. package/plugin/composer-mastermind/commands/evolve.md +4 -0
  171. package/plugin/composer-mastermind/hooks/boundary_guard.sh +43 -2
  172. package/plugin/composer-mastermind/hooks/codex_warm_review.sh +161 -9
  173. package/plugin/composer-mastermind/hooks/learn.sh +172 -32
  174. package/plugin/composer-mastermind/hooks/precommit_codex_review.sh +438 -64
  175. package/plugin/composer-mastermind/skills/composer-mastermind/SKILL.md +190 -4
  176. package/scripts/composer-oracle-router-safe.sh +47 -0
  177. package/scripts/composer-statusline-segment.mjs +40 -0
  178. package/scripts/oracle-codex-handoff-safe.sh +49 -0
  179. package/scripts/oracle-plan-mcp.sh +66 -0
  180. package/scripts/oracle-pro-safe.sh +572 -0
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # oracle-pro-safe.sh
5
+ # Safe ChatGPT Pro browser adapter for steipete/oracle.
6
+ # Key design: never assume optional/hidden Oracle flags. Probe them with a dry-run first,
7
+ # then include only flags accepted by the installed local binary.
8
+
9
+ usage() {
10
+ cat <<'USAGE'
11
+ Usage:
12
+ scripts/oracle-pro-safe.sh [options] -- "prompt"
13
+ scripts/oracle-pro-safe.sh [options] -p "prompt"
14
+
15
+ Options:
16
+ --mode <auto|quick|standard|deep|plan|review|debug|research>
17
+ --file <path-or-glob> Repeatable; passed to oracle --file
18
+ --slug <slug> Output slug prefix
19
+ --dry-run Run Oracle dry-run/preview only
20
+ --no-context Do not auto-attach lightweight repo context
21
+ --no-thinking-flag Do not pass --browser-thinking-time even if supported
22
+ --no-manual-login Do not pass --browser-manual-login even if supported
23
+ --model <model> Override selected Oracle model
24
+ --thinking <level> Override browser thinking level: light|standard|extended|heavy
25
+ --base <ref> Override branch-diff base ref for review-class modes
26
+ --research deep|off Override browser research mode
27
+ -p, --prompt <prompt> Prompt text
28
+ -h, --help
29
+
30
+ Environment overrides:
31
+ ORACLE_PRO_QUICK_MODEL default: gpt-5.2-instant
32
+ ORACLE_PRO_STANDARD_MODEL default: gpt-5.5
33
+ ORACLE_PRO_DEEP_MODEL default: gpt-5.5-pro
34
+ ORACLE_PRO_QUICK_THINKING default: light
35
+ ORACLE_PRO_STANDARD_THINKING default: standard
36
+ ORACLE_PRO_DEEP_THINKING default: extended
37
+ ORACLE_PRO_BROWSER_STRATEGY default: select
38
+ ORACLE_PRO_OUTPUT_DIR default: .composer/oracle/answers
39
+ ORACLE_PRO_CONTEXT_DIR default: .composer/oracle/context
40
+ ORACLE_PRO_TIMEOUT default: 20m
41
+ ORACLE_PRO_INPUT_TIMEOUT default: 60s
42
+ ORACLE_PRO_REATTACH_DELAY default: 30s
43
+ ORACLE_PRO_REATTACH_INTERVAL default: 2m
44
+ ORACLE_PRO_REATTACH_TIMEOUT default: 2m
45
+ ORACLE_PRO_ATTACHMENTS default: never (inline files; set to auto/bundle for large files)
46
+ ORACLE_PRO_DIFF_BASE default: auto-detect main/master/develop/origin HEAD
47
+ ORACLE_PRO_MAX_CHANGED_FILES default: 40
48
+ ORACLE_PRO_MAX_CHANGED_FILE_BYTES default: 120000
49
+
50
+ Secret file protection:
51
+ --file paths matching known secret patterns (.env, *.pem, *.key, id_rsa, .aws/credentials,
52
+ *secret*, *token*, *credential*, *password*, etc.) are rejected before upload.
53
+ Set ORACLE_PRO_ALLOW_SECRET_FILES=1 to override (use with caution).
54
+ USAGE
55
+ }
56
+
57
+ log() { printf '[oracle-pro] %s\n' "$*" >&2; }
58
+ warn() { printf '[oracle-pro][warn] %s\n' "$*" >&2; }
59
+ die() { printf '[oracle-pro][error] %s\n' "$*" >&2; exit 1; }
60
+
61
+ node_major() {
62
+ { "$1" --version 2>/dev/null || true; } | sed -E 's/^v?([0-9]+).*/\1/'
63
+ }
64
+
65
+ is_bad_node_major() {
66
+ # Keep this in sync with ORACLE_BAD_NODE_MAJORS in src/cli/doctor.ts.
67
+ case "$1" in
68
+ 26) return 0 ;;
69
+ *) return 1 ;;
70
+ esac
71
+ }
72
+
73
+ select_good_node() {
74
+ local node_bin major c version dir
75
+ if node_bin="$(command -v node 2>/dev/null)"; then
76
+ major="$(node_major "$node_bin")"
77
+ if [[ -n "$major" ]] && ! is_bad_node_major "$major"; then
78
+ return 0
79
+ fi
80
+ fi
81
+
82
+ shopt -u failglob nullglob
83
+
84
+ local candidates=(
85
+ "${ORACLE_NODE_BIN:-}"
86
+ /opt/homebrew/opt/node@24/bin/node
87
+ /usr/local/opt/node@24/bin/node
88
+ "$HOME"/.nvm/versions/node/v24*/bin/node
89
+ "$HOME"/.nvm/versions/node/v25*/bin/node
90
+ )
91
+
92
+ for c in "${candidates[@]}"; do
93
+ [[ -n "$c" && -x "$c" ]] || continue
94
+ major="$(node_major "$c")"
95
+ [[ -n "$major" ]] || continue
96
+ is_bad_node_major "$major" && continue
97
+ dir="$(dirname "$c")"
98
+ PATH="$dir:$PATH"
99
+ export PATH
100
+ version="$("$c" --version 2>/dev/null || true)"
101
+ log "pinned node $version from $dir (avoids undici setTypeOfService EINVAL)"
102
+ return 0
103
+ done
104
+
105
+ warn "no known-good node found; oracle may crash under bad node majors (undici setTypeOfService EINVAL)"
106
+ return 0
107
+ }
108
+
109
+ MODE="auto"
110
+ PROMPT=""
111
+ SLUG=""
112
+ DRY_RUN=0
113
+ AUTO_CONTEXT=1
114
+ USE_THINKING_FLAG=1
115
+ USE_MANUAL_LOGIN=1
116
+ MODEL_OVERRIDE=""
117
+ THINKING_OVERRIDE=""
118
+ RESEARCH_OVERRIDE=""
119
+ FILES=()
120
+ DIFF_BASE="${ORACLE_PRO_DIFF_BASE:-}"
121
+
122
+ while [[ $# -gt 0 ]]; do
123
+ case "$1" in
124
+ --mode) MODE="${2:-}"; shift 2 ;;
125
+ --mode=*) MODE="${1#*=}"; shift ;;
126
+ --file|-f) FILES+=("${2:-}"); shift 2 ;;
127
+ --file=*) FILES+=("${1#*=}"); shift ;;
128
+ --slug) SLUG="${2:-}"; shift 2 ;;
129
+ --slug=*) SLUG="${1#*=}"; shift ;;
130
+ --dry-run|--preview) DRY_RUN=1; shift ;;
131
+ --no-context) AUTO_CONTEXT=0; shift ;;
132
+ --no-thinking-flag) USE_THINKING_FLAG=0; shift ;;
133
+ --no-manual-login) USE_MANUAL_LOGIN=0; shift ;;
134
+ --model|-m) MODEL_OVERRIDE="${2:-}"; shift 2 ;;
135
+ --model=*) MODEL_OVERRIDE="${1#*=}"; shift ;;
136
+ --thinking) THINKING_OVERRIDE="${2:-}"; shift 2 ;;
137
+ --thinking=*) THINKING_OVERRIDE="${1#*=}"; shift ;;
138
+ --base) DIFF_BASE="${2:-}"; shift 2 ;;
139
+ --base=*) DIFF_BASE="${1#*=}"; shift ;;
140
+ --research) RESEARCH_OVERRIDE="${2:-}"; shift 2 ;;
141
+ --research=*) RESEARCH_OVERRIDE="${1#*=}"; shift ;;
142
+ -p|--prompt) PROMPT="${2:-}"; shift 2 ;;
143
+ --prompt=*) PROMPT="${1#*=}"; shift ;;
144
+ -h|--help) usage; exit 0 ;;
145
+ --) shift; PROMPT="${*:-}"; break ;;
146
+ *)
147
+ if [[ -z "$PROMPT" ]]; then PROMPT="$1"; else PROMPT="$PROMPT $1"; fi
148
+ shift
149
+ ;;
150
+ esac
151
+ done
152
+
153
+ [[ -n "$PROMPT" ]] || die "prompt is required"
154
+ command -v oracle >/dev/null 2>&1 || die "oracle not found in PATH"
155
+ select_good_node
156
+
157
+ case "$MODE" in
158
+ auto|quick|standard|deep|plan|review|debug|research) ;;
159
+ *) die "unknown mode: $MODE" ;;
160
+ esac
161
+
162
+ classify_mode() {
163
+ local text_lc
164
+ text_lc="$(printf '%s' "$PROMPT" | tr '[:upper:]' '[:lower:]')"
165
+ case "$text_lc" in
166
+ *'[oracle:quick]'*) echo quick; return ;;
167
+ *'[oracle:standard]'*) echo standard; return ;;
168
+ *'[oracle:deep]'*|*'[oracle:plan]'*) echo deep; return ;;
169
+ *'[oracle:review]'*) echo review; return ;;
170
+ *'[oracle:debug]'*) echo debug; return ;;
171
+ *'[oracle:research]'*) echo research; return ;;
172
+ esac
173
+ if [[ ${#PROMPT} -gt 2500 ]]; then echo deep; return; fi
174
+ if [[ "$text_lc" =~ (architecture|architectural|design|plan|planning|proposal|migration|refactor|roadmap|tradeoff|trade-off|spec|handoff|implementation[[:space:]]+plan) ]]; then echo deep; return; fi
175
+ if [[ "$text_lc" =~ (review|audit|regression|security|compatibility|api[[:space:]]+break|edge[[:space:]]+case|risk) ]]; then echo review; return; fi
176
+ if [[ "$text_lc" =~ (debug|root[ -]?cause|failing|failure|flaky|bug|stack[[:space:]]+trace|exception|crash|deadlock|race) ]]; then echo debug; return; fi
177
+ if [[ "$text_lc" =~ (research|compare[[:space:]]+options|survey|citations|latest|web) ]]; then echo research; return; fi
178
+ if [[ "$text_lc" =~ (quick|simple|small|syntax|command|explain) ]]; then echo quick; return; fi
179
+ echo standard
180
+ }
181
+
182
+ if [[ "$MODE" == "auto" ]]; then
183
+ MODE="$(classify_mode)"
184
+ fi
185
+
186
+ # Dispatch table. Model is the primary selector. Thinking flag is additive when the local binary supports it.
187
+ QUICK_MODEL="${ORACLE_PRO_QUICK_MODEL:-gpt-5.2-instant}"
188
+ STANDARD_MODEL="${ORACLE_PRO_STANDARD_MODEL:-gpt-5.5}"
189
+ DEEP_MODEL="${ORACLE_PRO_DEEP_MODEL:-gpt-5.5-pro}"
190
+ QUICK_THINKING="${ORACLE_PRO_QUICK_THINKING:-light}"
191
+ STANDARD_THINKING="${ORACLE_PRO_STANDARD_THINKING:-standard}"
192
+ DEEP_THINKING="${ORACLE_PRO_DEEP_THINKING:-extended}"
193
+ RESEARCH_MODE="off"
194
+ DEFAULT_STRATEGY="select"
195
+
196
+ case "$MODE" in
197
+ quick) MODEL="$QUICK_MODEL"; THINKING="$QUICK_THINKING"; DEFAULT_STRATEGY="current" ;;
198
+ standard) MODEL="$STANDARD_MODEL"; THINKING="$STANDARD_THINKING"; DEFAULT_STRATEGY="current" ;;
199
+ deep|plan|review|debug) MODEL="$DEEP_MODEL"; THINKING="$DEEP_THINKING"; DEFAULT_STRATEGY="select" ;;
200
+ research) MODEL="$DEEP_MODEL"; THINKING="$DEEP_THINKING"; RESEARCH_MODE="deep"; DEFAULT_STRATEGY="select" ;;
201
+ esac
202
+
203
+ [[ -z "$MODEL_OVERRIDE" ]] || MODEL="$MODEL_OVERRIDE"
204
+ [[ -z "$THINKING_OVERRIDE" ]] || THINKING="$THINKING_OVERRIDE"
205
+ [[ -z "$RESEARCH_OVERRIDE" ]] || RESEARCH_MODE="$RESEARCH_OVERRIDE"
206
+
207
+ # Delivery: quick/standard stay inline (fast, no upload-timeout risk); review-class
208
+ # modes default to `auto` (inline up to oracle's ~60k-char limit, then upload) so large
209
+ # branch diffs are no longer silently truncated. Override with ORACLE_PRO_ATTACHMENTS.
210
+ ATTACHMENTS_MODE="${ORACLE_PRO_ATTACHMENTS:-}"
211
+ if [[ -z "$ATTACHMENTS_MODE" ]]; then
212
+ case "$MODE" in
213
+ quick|standard) ATTACHMENTS_MODE="never" ;;
214
+ *) ATTACHMENTS_MODE="auto" ;;
215
+ esac
216
+ fi
217
+
218
+ # ChatGPT's Pro model auto-selects "Pro Extended"; its picker no longer exposes a
219
+ # separate thinking-time submenu, so --browser-thinking-time errors out with
220
+ # "Thinking time: menu not found for pro (requested ...)" and oracle exits 1.
221
+ # The flag is redundant for the Pro model, so skip it there. Override with
222
+ # ORACLE_PRO_FORCE_THINKING_FLAG=1 if a future oracle/UI restores the menu.
223
+ case "$MODEL" in
224
+ *pro*)
225
+ if [[ "${ORACLE_PRO_FORCE_THINKING_FLAG:-0}" != "1" ]]; then
226
+ USE_THINKING_FLAG=0
227
+ fi
228
+ ;;
229
+ esac
230
+
231
+ OUT_DIR="${ORACLE_PRO_OUTPUT_DIR:-.composer/oracle/answers}"
232
+ CTX_DIR="${ORACLE_PRO_CONTEXT_DIR:-.composer/oracle/context}"
233
+ mkdir -p "$OUT_DIR" "$CTX_DIR"
234
+
235
+ is_secret_file() {
236
+ # Returns 0 (true) if $1 looks like a secret/credential file we must not upload.
237
+ local p base lc
238
+ p="$1"
239
+ base="${p##*/}"
240
+ lc="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')"
241
+ case "$lc" in
242
+ .env|.env.*|*.pem|*.key|*.p12|*.pfx|*.keystore|*.jks|*.kdbx|*.ppk|id_rsa|id_dsa|id_ecdsa|id_ed25519|.npmrc|.netrc|.pgpass)
243
+ return 0 ;;
244
+ *secret*|*token*|*credential*|*password*)
245
+ return 0 ;;
246
+ esac
247
+ case "$p" in
248
+ */.ssh/*|*/.aws/credentials|*/.gnupg/*)
249
+ return 0 ;;
250
+ esac
251
+ return 1
252
+ }
253
+
254
+ safe_slug() {
255
+ local s="$1"
256
+ s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+|-+$//g; s/-+/-/g')"
257
+ [[ -n "$s" ]] || s="oracle"
258
+ printf '%s' "${s:0:80}"
259
+ }
260
+
261
+ if [[ -z "$SLUG" ]]; then
262
+ SLUG="$(date +%Y%m%d-%H%M%S)-${MODE}"
263
+ else
264
+ SLUG="$(date +%Y%m%d-%H%M%S)-$(safe_slug "$SLUG")"
265
+ fi
266
+ OUT_FILE="$OUT_DIR/$SLUG.md"
267
+
268
+ # Probe support for optional flags. A dry-run should parse options without touching Chrome.
269
+ # Cache per-version-ish in the context dir to avoid repeated probes.
270
+ ORACLE_VERSION="$(oracle --version 2>/dev/null | head -1 | tr -d '\r' || true)"
271
+ CACHE_DIR="$CTX_DIR/.flag-cache"
272
+ mkdir -p "$CACHE_DIR"
273
+ cache_key="$(printf '%s' "$(command -v oracle)::${ORACLE_VERSION}" | shasum 2>/dev/null | awk '{print $1}')"
274
+ [[ -n "$cache_key" ]] || cache_key="default"
275
+
276
+ supports_option() {
277
+ local flag="$1"
278
+ local value="${2-}"
279
+ local key="$CACHE_DIR/${cache_key}.$(printf '%s' "$flag" | tr -c 'a-zA-Z0-9_' '_')"
280
+ if [[ -f "$key" ]]; then
281
+ [[ "$(cat "$key")" == "yes" ]]
282
+ return
283
+ fi
284
+ local args=(--engine browser --dry-run summary -p "oracle flag probe")
285
+ if [[ -n "$value" ]]; then args+=("$flag" "$value"); else args+=("$flag"); fi
286
+ if oracle "${args[@]}" >/dev/null 2>"$key.err"; then
287
+ printf 'yes' > "$key"
288
+ return 0
289
+ fi
290
+ if grep -qiE 'unknown option|unknown argument|invalid option' "$key.err" 2>/dev/null; then
291
+ printf 'no' > "$key"
292
+ return 1
293
+ fi
294
+ # Conservative: if dry-run failed for some non-parse reason, do not use the optional flag.
295
+ printf 'no' > "$key"
296
+ return 1
297
+ }
298
+
299
+ add_supported_flag() {
300
+ # $1 is the target array name (historically a nameref); all callers use ARGS,
301
+ # so append to ARGS directly to stay compatible with Bash 3.2 (no `local -n`).
302
+ local flag="$2"
303
+ local value="${3-}"
304
+ if supports_option "$flag" "$value"; then
305
+ if [[ -n "$value" ]]; then ARGS+=("$flag" "$value"); else ARGS+=("$flag"); fi
306
+ else
307
+ warn "installed oracle does not accept $flag; skipping"
308
+ fi
309
+ }
310
+
311
+ # Auto context: small, local, non-secret evidence that helps ChatGPT plan/review without dumping the repo.
312
+ AUTO_FILES=()
313
+ write_context_file() {
314
+ local name="$1"
315
+ local content_cmd="$2"
316
+ local path="$CTX_DIR/$SLUG.$name"
317
+ bash -lc "$content_cmd" > "$path" 2>/dev/null || true
318
+ if [[ -s "$path" ]]; then AUTO_FILES+=("$path"); fi
319
+ }
320
+
321
+ detect_diff_base() {
322
+ local b
323
+ if [[ -n "$DIFF_BASE" ]]; then printf '%s' "$DIFF_BASE"; return; fi
324
+ for b in main master develop; do
325
+ git rev-parse --verify --quiet "refs/heads/$b" >/dev/null 2>&1 && { printf '%s' "$b"; return; }
326
+ done
327
+ for b in origin/main origin/master origin/develop; do
328
+ git rev-parse --verify --quiet "refs/remotes/$b" >/dev/null 2>&1 && { printf '%s' "$b"; return; }
329
+ done
330
+ git symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null | sed 's#refs/remotes/##' || true
331
+ }
332
+
333
+ # Populates the global SECRET_EXCLUDES array with :(exclude,literal)<path> pathspecs
334
+ # for every changed file that is_secret_file rejects, giving git-diff patch generation
335
+ # the SAME denylist coverage as explicit --file uploads. Args: optional git diff range.
336
+ build_secret_excludes() {
337
+ SECRET_EXCLUDES=()
338
+ [[ "${ORACLE_PRO_ALLOW_SECRET_FILES:-0}" == "1" ]] && return 0
339
+ local cf
340
+ while IFS= read -r cf; do
341
+ [[ -n "$cf" ]] || continue
342
+ if is_secret_file "$cf"; then SECRET_EXCLUDES+=(":(exclude,literal)$cf"); fi
343
+ done < <(git diff --name-only "$@" 2>/dev/null)
344
+ }
345
+
346
+ if [[ "$AUTO_CONTEXT" -eq 1 ]]; then
347
+ # Authority class B — current policy/context (safe as source-of-truth).
348
+ policy_files=(CLAUDE.md composer.config.json docs/STATUS.md)
349
+ # Authority class C — background/history (risky as source-of-truth; only for planning/research).
350
+ background_files=(README.md AGENTS.md)
351
+ # Project manifest files (dependency intent + language).
352
+ base_files=(package.json pyproject.toml Cargo.toml go.mod)
353
+
354
+ # Task-aware attach set: minimal for trivial modes, no stale background for review/debug.
355
+ attach_candidates=()
356
+ case "$MODE" in
357
+ quick|standard)
358
+ attach_candidates=(CLAUDE.md docs/STATUS.md)
359
+ ;;
360
+ review|debug)
361
+ attach_candidates=("${policy_files[@]}" "${base_files[@]}")
362
+ ;;
363
+ deep|plan|research|*)
364
+ attach_candidates=("${policy_files[@]}" "${background_files[@]}" "${base_files[@]}")
365
+ ;;
366
+ esac
367
+
368
+ for candidate in "${attach_candidates[@]}"; do
369
+ [[ -f "$candidate" ]] && AUTO_FILES+=("$candidate")
370
+ done
371
+
372
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
373
+ write_context_file "git-status.txt" "git status --short"
374
+ write_context_file "git-diff-stat.txt" "git diff --stat"
375
+ if [[ "$MODE" != "quick" && "$MODE" != "standard" ]]; then
376
+ build_secret_excludes
377
+ wt_patch="$CTX_DIR/$SLUG.git-diff.patch"
378
+ git diff -- . ':(exclude).env' ':(exclude).env.*' ':(exclude)*.pem' ':(exclude)*.key' ':(exclude)*.p12' \
379
+ ${SECRET_EXCLUDES[@]+"${SECRET_EXCLUDES[@]}"} 2>/dev/null \
380
+ | sed -E -e '/(api[_-]?key|secret|token|passwd|password|credential|authorization|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key)[^a-z0-9]{0,4}[:=]/Id' -e '/bearer[[:space:]]+[a-z0-9._-]{6,}/Id' \
381
+ | head -c 200000 > "$wt_patch" 2>/dev/null || true
382
+ [[ -s "$wt_patch" ]] && AUTO_FILES+=("$wt_patch")
383
+ fi
384
+ # Exact installed top-level deps (package.json shows ranges, not the installed tree).
385
+ if [[ "$MODE" != "quick" && "$MODE" != "standard" ]] && [[ -f package.json ]]; then
386
+ write_context_file "deps.txt" "npm ls --depth=0 2>/dev/null || true"
387
+ fi
388
+ if [[ "$MODE" != "quick" && "$MODE" != "standard" ]]; then
389
+ DIFF_BASE_RESOLVED="$(detect_diff_base)"
390
+ # Defense-in-depth: a git ref can legally contain shell metacharacters
391
+ # (`;`, `$()`, backticks). Since the ref is later interpolated into commands,
392
+ # reject anything outside a safe charset and forbid a leading dash (git option
393
+ # injection) before use.
394
+ if [[ -n "$DIFF_BASE_RESOLVED" && ! "$DIFF_BASE_RESOLVED" =~ ^[A-Za-z0-9._/][A-Za-z0-9._/-]*$ ]]; then
395
+ warn "ignoring unsafe diff-base ref (disallowed characters): $DIFF_BASE_RESOLVED"
396
+ DIFF_BASE_RESOLVED=""
397
+ fi
398
+ CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"
399
+ if [[ -n "$DIFF_BASE_RESOLVED" && "$CUR_BRANCH" != "$DIFF_BASE_RESOLVED" ]] \
400
+ && git rev-parse --verify --quiet "$DIFF_BASE_RESOLVED" >/dev/null 2>&1; then
401
+ # The committed branch diff vs its base is the REAL review target (working-tree diff
402
+ # is empty once work is committed). This is the primary accuracy fix.
403
+ build_secret_excludes "$DIFF_BASE_RESOLVED...HEAD"
404
+ base_patch="$CTX_DIR/$SLUG.branch-diff.patch"
405
+ git diff "$DIFF_BASE_RESOLVED...HEAD" -- . ':(exclude).env' ':(exclude).env.*' ':(exclude)*.pem' ':(exclude)*.key' ':(exclude)*.p12' \
406
+ ${SECRET_EXCLUDES[@]+"${SECRET_EXCLUDES[@]}"} 2>/dev/null \
407
+ | sed -E -e '/(api[_-]?key|secret|token|passwd|password|credential|authorization|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key)[^a-z0-9]{0,4}[:=]/Id' -e '/bearer[[:space:]]+[a-z0-9._-]{6,}/Id' \
408
+ | head -c 400000 > "$base_patch" 2>/dev/null || true
409
+ [[ -s "$base_patch" ]] && AUTO_FILES+=("$base_patch")
410
+ write_context_file "branch-diff-stat.txt" "git diff ${DIFF_BASE_RESOLVED}...HEAD --stat"
411
+ # Attach the full content of changed files (enclosing scope for the diff hunks),
412
+ # capped by count and per-file size, secret-filtered.
413
+ while IFS= read -r changed; do
414
+ [[ -n "$changed" && -f "$changed" ]] || continue
415
+ if is_secret_file "$changed" && [[ "${ORACLE_PRO_ALLOW_SECRET_FILES:-0}" != "1" ]]; then continue; fi
416
+ csz="$(wc -c < "$changed" 2>/dev/null | tr -d ' ')"
417
+ [[ -n "$csz" && "$csz" -le "${ORACLE_PRO_MAX_CHANGED_FILE_BYTES:-120000}" ]] || continue
418
+ AUTO_FILES+=("$changed")
419
+ done < <(git diff --name-only "${DIFF_BASE_RESOLVED}...HEAD" -- . ':(exclude).env' ':(exclude).env.*' 2>/dev/null | head -n "${ORACLE_PRO_MAX_CHANGED_FILES:-40}")
420
+ fi
421
+ fi
422
+ fi
423
+ fi
424
+
425
+ ARGS=(--engine browser -m "$MODEL" --write-output "$OUT_FILE")
426
+
427
+ if [[ "$DRY_RUN" -eq 1 ]]; then
428
+ ARGS+=(--dry-run summary)
429
+ fi
430
+
431
+ # Official v0.13.0 accepts these, but many are hidden from --help; still probe to survive forks/older installs.
432
+ add_supported_flag ARGS --browser-model-strategy "${ORACLE_PRO_BROWSER_STRATEGY:-$DEFAULT_STRATEGY}"
433
+
434
+ if [[ "$USE_THINKING_FLAG" -eq 1 ]]; then
435
+ add_supported_flag ARGS --browser-thinking-time "$THINKING"
436
+ fi
437
+
438
+ if [[ "$RESEARCH_MODE" == "deep" ]]; then
439
+ add_supported_flag ARGS --browser-research deep
440
+ fi
441
+
442
+ if [[ "$USE_MANUAL_LOGIN" -eq 1 ]]; then
443
+ add_supported_flag ARGS --browser-manual-login
444
+ fi
445
+
446
+ add_supported_flag ARGS --browser-timeout "${ORACLE_PRO_TIMEOUT:-20m}"
447
+ add_supported_flag ARGS --browser-input-timeout "${ORACLE_PRO_INPUT_TIMEOUT:-60s}"
448
+ add_supported_flag ARGS --browser-attachments "$ATTACHMENTS_MODE"
449
+ if [[ "$ATTACHMENTS_MODE" != "never" ]]; then
450
+ add_supported_flag ARGS --browser-bundle-format "${ORACLE_PRO_BUNDLE_FORMAT:-text}"
451
+ fi
452
+ add_supported_flag ARGS --browser-auto-reattach-delay "${ORACLE_PRO_REATTACH_DELAY:-30s}"
453
+ add_supported_flag ARGS --browser-auto-reattach-interval "${ORACLE_PRO_REATTACH_INTERVAL:-2m}"
454
+ add_supported_flag ARGS --browser-auto-reattach-timeout "${ORACLE_PRO_REATTACH_TIMEOUT:-2m}"
455
+ add_supported_flag ARGS --heartbeat "${ORACLE_PRO_HEARTBEAT:-30}"
456
+
457
+ # Validate + collect the exact attachment set (user files first, then auto context).
458
+ ATTACH=()
459
+ for f in "${FILES[@]}"; do
460
+ [[ -n "$f" ]] || continue
461
+ if is_secret_file "$f" && [[ "${ORACLE_PRO_ALLOW_SECRET_FILES:-0}" != "1" ]]; then
462
+ die "refusing to upload potential secret file: $f (matches secret denylist). Rename/relocate it, or set ORACLE_PRO_ALLOW_SECRET_FILES=1 to override."
463
+ fi
464
+ ATTACH+=("$f")
465
+ done
466
+ for f in "${AUTO_FILES[@]}"; do
467
+ [[ -n "$f" ]] && ATTACH+=("$f")
468
+ done
469
+
470
+ # Snapshot manifest: authoritative identity of the repo state this call may discuss.
471
+ # Lets the model (and us) detect drift between turns and bounds it to current disk state.
472
+ captured_at="$(date +%Y-%m-%dT%H:%M:%S%z)"
473
+ repo_root="$(pwd)"
474
+ git_branch="n/a"; git_head="n/a"; dirty="false"; status_hash="n/a"; diff_hash="n/a"
475
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
476
+ git_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo n/a)"
477
+ git_head="$(git rev-parse HEAD 2>/dev/null || echo n/a)"
478
+ [[ -n "$(git status --porcelain 2>/dev/null)" ]] && dirty="true"
479
+ status_hash="$(git status --short 2>/dev/null | shasum -a 256 2>/dev/null | awk '{print $1}')"
480
+ diff_hash="$(git diff 2>/dev/null | shasum -a 256 2>/dev/null | awk '{print $1}')"
481
+ fi
482
+ [[ -n "$status_hash" ]] || status_hash="n/a"
483
+ [[ -n "$diff_hash" ]] || diff_hash="n/a"
484
+ node_ver="$(node --version 2>/dev/null || echo n/a)"
485
+ npm_ver="$(npm --version 2>/dev/null || echo n/a)"
486
+ os_ver="$(uname -sr 2>/dev/null || echo n/a)"
487
+ arch_ver="$(uname -m 2>/dev/null || echo n/a)"
488
+ short_head="${git_head:0:12}"
489
+ repo_state_hash="$(printf '%s' "${git_head}${status_hash}${diff_hash}${node_ver}${npm_ver}" | shasum -a 256 2>/dev/null | awk '{print $1}')"
490
+ [[ -n "$repo_state_hash" ]] || repo_state_hash="n/a"
491
+ snapshot_id="${SLUG}-${short_head}-${repo_state_hash:0:8}"
492
+
493
+ MANIFEST_PATH="$CTX_DIR/$SLUG.manifest.json"
494
+ # JSON-escape a string value (backslash and double-quote only; inputs are paths/hashes/versions).
495
+ json_escape() { printf '%s' "${1-}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
496
+ {
497
+ printf '{\n'
498
+ printf ' "snapshotId": "%s",\n' "$(json_escape "$snapshot_id")"
499
+ printf ' "capturedAt": "%s",\n' "$(json_escape "$captured_at")"
500
+ printf ' "repoRoot": "%s",\n' "$(json_escape "$repo_root")"
501
+ printf ' "branch": "%s",\n' "$(json_escape "$git_branch")"
502
+ printf ' "head": "%s",\n' "$(json_escape "$git_head")"
503
+ printf ' "dirty": %s,\n' "$dirty"
504
+ printf ' "gitStatusHash": "%s",\n' "$(json_escape "$status_hash")"
505
+ printf ' "diffHash": "%s",\n' "$(json_escape "$diff_hash")"
506
+ printf ' "repoStateHash": "%s",\n' "$(json_escape "$repo_state_hash")"
507
+ printf ' "mode": "%s",\n' "$(json_escape "$MODE")"
508
+ printf ' "runtime": { "node": "%s", "npm": "%s", "os": "%s", "arch": "%s" },\n' \
509
+ "$(json_escape "$node_ver")" "$(json_escape "$npm_ver")" "$(json_escape "$os_ver")" "$(json_escape "$arch_ver")"
510
+ printf ' "attachments": [\n'
511
+ manifest_n=${#ATTACH[@]}
512
+ manifest_i=0
513
+ for f in "${ATTACH[@]}"; do
514
+ manifest_i=$((manifest_i + 1))
515
+ sha="$(shasum -a 256 "$f" 2>/dev/null | awk '{print $1}')"
516
+ bytes="$(wc -c < "$f" 2>/dev/null | tr -d ' ')"
517
+ sep=","
518
+ [[ "$manifest_i" -eq "$manifest_n" ]] && sep=""
519
+ printf ' { "path": "%s", "sha256": "%s", "bytes": %s }%s\n' \
520
+ "$(json_escape "$f")" "${sha:-}" "${bytes:-0}" "$sep"
521
+ done
522
+ printf ' ]\n'
523
+ printf '}\n'
524
+ } > "$MANIFEST_PATH" 2>/dev/null || warn "manifest generation failed (continuing without manifest)"
525
+ [[ -f "$MANIFEST_PATH" ]] && ATTACH+=("$MANIFEST_PATH")
526
+
527
+ # CONTEXT CONTRACT: bound the model to this snapshot; defeat stale memory/attachments.
528
+ CONTRACT="$(cat <<EOF
529
+ CONTEXT CONTRACT
530
+ Authoritative snapshot: ${snapshot_id}
531
+ Captured at: ${captured_at}
532
+ Branch / HEAD: ${git_branch} / ${git_head}
533
+ Dirty tree: ${dirty}
534
+ Repo-state hash: ${repo_state_hash}
535
+ Runtime: node=${node_ver} npm=${npm_ver} os=${os_ver} arch=${arch_ver}
536
+ Task: ${MODE}
537
+ Authority order:
538
+ A. Live source-of-truth: ${SLUG}.manifest.json, ${SLUG}.git-status.txt, ${SLUG}.git-diff.patch, ${SLUG}.branch-diff.patch, ${SLUG}.deps.txt, changed source files, targeted source/tests
539
+ B. Current policy/context: CLAUDE.md, composer.config.json, docs/STATUS.md, relevant ADRs
540
+ C. Background/history: README.md, AGENTS.md
541
+ Rules:
542
+ - Treat class A as authoritative for the local repo. If A conflicts with B or C, A wins.
543
+ - The attached files are the ONLY authoritative source for any code-level claim. Any description of the code in the TASK below is a hint about what to review, NOT source of truth — never treat prose as code.
544
+ - Ignore prior chat memory, project memory, and earlier attachments if they conflict with this snapshot.
545
+ - For each substantive claim, tag it [attached], [runtime], [web], or [inference].
546
+ - Cite attached claims with file path and line span.
547
+ - For EACH finding: first quote the exact supporting line(s) verbatim from an attached file as \`path:line\`, THEN state the finding. If you cannot quote supporting lines from the attached files, label it "INSUFFICIENT EVIDENCE — not in provided context" and do not raise it as a blocker.
548
+ - Do not infer a bug from an absent detail; absence of code in the attachments means unknown, not broken.
549
+ - For current API/library claims not proven by attached files, verify on the web against primary docs.
550
+ - If evidence is insufficient, say: "unknown from provided context".
551
+ EOF
552
+ )"
553
+ PROMPT="${CONTRACT}
554
+
555
+ ${PROMPT}"
556
+
557
+ # Emit all attachments (incl. the manifest) as --file inputs.
558
+ for f in "${ATTACH[@]}"; do
559
+ [[ -n "$f" ]] && ARGS+=(--file "$f")
560
+ done
561
+
562
+ log "oracle version: ${ORACLE_VERSION:-unknown}"
563
+ log "mode=$MODE model=$MODEL thinking=$THINKING research=$RESEARCH_MODE output=$OUT_FILE"
564
+ log "files: user=${#FILES[@]} auto=${#AUTO_FILES[@]} total=${#ATTACH[@]} snapshot=${snapshot_id}"
565
+
566
+ oracle "${ARGS[@]}" -p "$PROMPT"
567
+
568
+ # Maintain a stable latest file for downstream Codex/Composer commands.
569
+ if [[ -f "$OUT_FILE" ]]; then
570
+ cp "$OUT_FILE" "$OUT_DIR/latest.md"
571
+ printf '%s\n' "$OUT_FILE"
572
+ fi