agent-composer 0.3.1 → 0.4.0

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 +430 -62
  175. package/plugin/composer-mastermind/skills/composer-mastermind/SKILL.md +184 -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 +471 -0
@@ -0,0 +1,471 @@
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
+ --research deep|off Override browser research mode
26
+ -p, --prompt <prompt> Prompt text
27
+ -h, --help
28
+
29
+ Environment overrides:
30
+ ORACLE_PRO_QUICK_MODEL default: gpt-5.2-instant
31
+ ORACLE_PRO_STANDARD_MODEL default: gpt-5.5
32
+ ORACLE_PRO_DEEP_MODEL default: gpt-5.5-pro
33
+ ORACLE_PRO_QUICK_THINKING default: light
34
+ ORACLE_PRO_STANDARD_THINKING default: standard
35
+ ORACLE_PRO_DEEP_THINKING default: extended
36
+ ORACLE_PRO_BROWSER_STRATEGY default: select
37
+ ORACLE_PRO_OUTPUT_DIR default: .composer/oracle/answers
38
+ ORACLE_PRO_CONTEXT_DIR default: .composer/oracle/context
39
+ ORACLE_PRO_TIMEOUT default: 20m
40
+ ORACLE_PRO_INPUT_TIMEOUT default: 60s
41
+ ORACLE_PRO_REATTACH_DELAY default: 30s
42
+ ORACLE_PRO_REATTACH_INTERVAL default: 2m
43
+ ORACLE_PRO_REATTACH_TIMEOUT default: 2m
44
+ ORACLE_PRO_ATTACHMENTS default: never (inline files; set to auto/bundle for large files)
45
+
46
+ Secret file protection:
47
+ --file paths matching known secret patterns (.env, *.pem, *.key, id_rsa, .aws/credentials,
48
+ *secret*, *token*, *credential*, *password*, etc.) are rejected before upload.
49
+ Set ORACLE_PRO_ALLOW_SECRET_FILES=1 to override (use with caution).
50
+ USAGE
51
+ }
52
+
53
+ log() { printf '[oracle-pro] %s\n' "$*" >&2; }
54
+ warn() { printf '[oracle-pro][warn] %s\n' "$*" >&2; }
55
+ die() { printf '[oracle-pro][error] %s\n' "$*" >&2; exit 1; }
56
+
57
+ node_major() {
58
+ { "$1" --version 2>/dev/null || true; } | sed -E 's/^v?([0-9]+).*/\1/'
59
+ }
60
+
61
+ is_bad_node_major() {
62
+ # Keep this in sync with ORACLE_BAD_NODE_MAJORS in src/cli/doctor.ts.
63
+ case "$1" in
64
+ 26) return 0 ;;
65
+ *) return 1 ;;
66
+ esac
67
+ }
68
+
69
+ select_good_node() {
70
+ local node_bin major c version dir
71
+ if node_bin="$(command -v node 2>/dev/null)"; then
72
+ major="$(node_major "$node_bin")"
73
+ if [[ -n "$major" ]] && ! is_bad_node_major "$major"; then
74
+ return 0
75
+ fi
76
+ fi
77
+
78
+ shopt -u failglob nullglob
79
+
80
+ local candidates=(
81
+ "${ORACLE_NODE_BIN:-}"
82
+ /opt/homebrew/opt/node@24/bin/node
83
+ /usr/local/opt/node@24/bin/node
84
+ "$HOME"/.nvm/versions/node/v24*/bin/node
85
+ "$HOME"/.nvm/versions/node/v25*/bin/node
86
+ )
87
+
88
+ for c in "${candidates[@]}"; do
89
+ [[ -n "$c" && -x "$c" ]] || continue
90
+ major="$(node_major "$c")"
91
+ [[ -n "$major" ]] || continue
92
+ is_bad_node_major "$major" && continue
93
+ dir="$(dirname "$c")"
94
+ PATH="$dir:$PATH"
95
+ export PATH
96
+ version="$("$c" --version 2>/dev/null || true)"
97
+ log "pinned node $version from $dir (avoids undici setTypeOfService EINVAL)"
98
+ return 0
99
+ done
100
+
101
+ warn "no known-good node found; oracle may crash under bad node majors (undici setTypeOfService EINVAL)"
102
+ return 0
103
+ }
104
+
105
+ MODE="auto"
106
+ PROMPT=""
107
+ SLUG=""
108
+ DRY_RUN=0
109
+ AUTO_CONTEXT=1
110
+ USE_THINKING_FLAG=1
111
+ USE_MANUAL_LOGIN=1
112
+ MODEL_OVERRIDE=""
113
+ THINKING_OVERRIDE=""
114
+ RESEARCH_OVERRIDE=""
115
+ FILES=()
116
+
117
+ while [[ $# -gt 0 ]]; do
118
+ case "$1" in
119
+ --mode) MODE="${2:-}"; shift 2 ;;
120
+ --mode=*) MODE="${1#*=}"; shift ;;
121
+ --file|-f) FILES+=("${2:-}"); shift 2 ;;
122
+ --file=*) FILES+=("${1#*=}"); shift ;;
123
+ --slug) SLUG="${2:-}"; shift 2 ;;
124
+ --slug=*) SLUG="${1#*=}"; shift ;;
125
+ --dry-run|--preview) DRY_RUN=1; shift ;;
126
+ --no-context) AUTO_CONTEXT=0; shift ;;
127
+ --no-thinking-flag) USE_THINKING_FLAG=0; shift ;;
128
+ --no-manual-login) USE_MANUAL_LOGIN=0; shift ;;
129
+ --model|-m) MODEL_OVERRIDE="${2:-}"; shift 2 ;;
130
+ --model=*) MODEL_OVERRIDE="${1#*=}"; shift ;;
131
+ --thinking) THINKING_OVERRIDE="${2:-}"; shift 2 ;;
132
+ --thinking=*) THINKING_OVERRIDE="${1#*=}"; shift ;;
133
+ --research) RESEARCH_OVERRIDE="${2:-}"; shift 2 ;;
134
+ --research=*) RESEARCH_OVERRIDE="${1#*=}"; shift ;;
135
+ -p|--prompt) PROMPT="${2:-}"; shift 2 ;;
136
+ --prompt=*) PROMPT="${1#*=}"; shift ;;
137
+ -h|--help) usage; exit 0 ;;
138
+ --) shift; PROMPT="${*:-}"; break ;;
139
+ *)
140
+ if [[ -z "$PROMPT" ]]; then PROMPT="$1"; else PROMPT="$PROMPT $1"; fi
141
+ shift
142
+ ;;
143
+ esac
144
+ done
145
+
146
+ [[ -n "$PROMPT" ]] || die "prompt is required"
147
+ command -v oracle >/dev/null 2>&1 || die "oracle not found in PATH"
148
+ select_good_node
149
+
150
+ case "$MODE" in
151
+ auto|quick|standard|deep|plan|review|debug|research) ;;
152
+ *) die "unknown mode: $MODE" ;;
153
+ esac
154
+
155
+ classify_mode() {
156
+ local text_lc
157
+ text_lc="$(printf '%s' "$PROMPT" | tr '[:upper:]' '[:lower:]')"
158
+ case "$text_lc" in
159
+ *'[oracle:quick]'*) echo quick; return ;;
160
+ *'[oracle:standard]'*) echo standard; return ;;
161
+ *'[oracle:deep]'*|*'[oracle:plan]'*) echo deep; return ;;
162
+ *'[oracle:review]'*) echo review; return ;;
163
+ *'[oracle:debug]'*) echo debug; return ;;
164
+ *'[oracle:research]'*) echo research; return ;;
165
+ esac
166
+ if [[ ${#PROMPT} -gt 2500 ]]; then echo deep; return; fi
167
+ 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
168
+ if [[ "$text_lc" =~ (review|audit|regression|security|compatibility|api[[:space:]]+break|edge[[:space:]]+case|risk) ]]; then echo review; return; fi
169
+ if [[ "$text_lc" =~ (debug|root[ -]?cause|failing|failure|flaky|bug|stack[[:space:]]+trace|exception|crash|deadlock|race) ]]; then echo debug; return; fi
170
+ if [[ "$text_lc" =~ (research|compare[[:space:]]+options|survey|citations|latest|web) ]]; then echo research; return; fi
171
+ if [[ "$text_lc" =~ (quick|simple|small|syntax|command|explain) ]]; then echo quick; return; fi
172
+ echo standard
173
+ }
174
+
175
+ if [[ "$MODE" == "auto" ]]; then
176
+ MODE="$(classify_mode)"
177
+ fi
178
+
179
+ # Dispatch table. Model is the primary selector. Thinking flag is additive when the local binary supports it.
180
+ QUICK_MODEL="${ORACLE_PRO_QUICK_MODEL:-gpt-5.2-instant}"
181
+ STANDARD_MODEL="${ORACLE_PRO_STANDARD_MODEL:-gpt-5.5}"
182
+ DEEP_MODEL="${ORACLE_PRO_DEEP_MODEL:-gpt-5.5-pro}"
183
+ QUICK_THINKING="${ORACLE_PRO_QUICK_THINKING:-light}"
184
+ STANDARD_THINKING="${ORACLE_PRO_STANDARD_THINKING:-standard}"
185
+ DEEP_THINKING="${ORACLE_PRO_DEEP_THINKING:-extended}"
186
+ RESEARCH_MODE="off"
187
+ DEFAULT_STRATEGY="select"
188
+ ATTACHMENTS_MODE="${ORACLE_PRO_ATTACHMENTS:-never}"
189
+
190
+ case "$MODE" in
191
+ quick) MODEL="$QUICK_MODEL"; THINKING="$QUICK_THINKING"; DEFAULT_STRATEGY="current" ;;
192
+ standard) MODEL="$STANDARD_MODEL"; THINKING="$STANDARD_THINKING"; DEFAULT_STRATEGY="current" ;;
193
+ deep|plan|review|debug) MODEL="$DEEP_MODEL"; THINKING="$DEEP_THINKING"; DEFAULT_STRATEGY="select" ;;
194
+ research) MODEL="$DEEP_MODEL"; THINKING="$DEEP_THINKING"; RESEARCH_MODE="deep"; DEFAULT_STRATEGY="select" ;;
195
+ esac
196
+
197
+ [[ -z "$MODEL_OVERRIDE" ]] || MODEL="$MODEL_OVERRIDE"
198
+ [[ -z "$THINKING_OVERRIDE" ]] || THINKING="$THINKING_OVERRIDE"
199
+ [[ -z "$RESEARCH_OVERRIDE" ]] || RESEARCH_MODE="$RESEARCH_OVERRIDE"
200
+
201
+ OUT_DIR="${ORACLE_PRO_OUTPUT_DIR:-.composer/oracle/answers}"
202
+ CTX_DIR="${ORACLE_PRO_CONTEXT_DIR:-.composer/oracle/context}"
203
+ mkdir -p "$OUT_DIR" "$CTX_DIR"
204
+
205
+ is_secret_file() {
206
+ # Returns 0 (true) if $1 looks like a secret/credential file we must not upload.
207
+ local p base lc
208
+ p="$1"
209
+ base="${p##*/}"
210
+ lc="$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')"
211
+ case "$lc" in
212
+ .env|.env.*|*.pem|*.key|*.p12|*.pfx|*.keystore|*.jks|*.kdbx|*.ppk|id_rsa|id_dsa|id_ecdsa|id_ed25519|.npmrc|.netrc|.pgpass)
213
+ return 0 ;;
214
+ *secret*|*token*|*credential*|*password*)
215
+ return 0 ;;
216
+ esac
217
+ case "$p" in
218
+ */.ssh/*|*/.aws/credentials|*/.gnupg/*)
219
+ return 0 ;;
220
+ esac
221
+ return 1
222
+ }
223
+
224
+ safe_slug() {
225
+ local s="$1"
226
+ s="$(printf '%s' "$s" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+|-+$//g; s/-+/-/g')"
227
+ [[ -n "$s" ]] || s="oracle"
228
+ printf '%s' "${s:0:80}"
229
+ }
230
+
231
+ if [[ -z "$SLUG" ]]; then
232
+ SLUG="$(date +%Y%m%d-%H%M%S)-${MODE}"
233
+ else
234
+ SLUG="$(date +%Y%m%d-%H%M%S)-$(safe_slug "$SLUG")"
235
+ fi
236
+ OUT_FILE="$OUT_DIR/$SLUG.md"
237
+
238
+ # Probe support for optional flags. A dry-run should parse options without touching Chrome.
239
+ # Cache per-version-ish in the context dir to avoid repeated probes.
240
+ ORACLE_VERSION="$(oracle --version 2>/dev/null | head -1 | tr -d '\r' || true)"
241
+ CACHE_DIR="$CTX_DIR/.flag-cache"
242
+ mkdir -p "$CACHE_DIR"
243
+ cache_key="$(printf '%s' "$(command -v oracle)::${ORACLE_VERSION}" | shasum 2>/dev/null | awk '{print $1}')"
244
+ [[ -n "$cache_key" ]] || cache_key="default"
245
+
246
+ supports_option() {
247
+ local flag="$1"
248
+ local value="${2-}"
249
+ local key="$CACHE_DIR/${cache_key}.$(printf '%s' "$flag" | tr -c 'a-zA-Z0-9_' '_')"
250
+ if [[ -f "$key" ]]; then
251
+ [[ "$(cat "$key")" == "yes" ]]
252
+ return
253
+ fi
254
+ local args=(--engine browser --dry-run summary -p "oracle flag probe")
255
+ if [[ -n "$value" ]]; then args+=("$flag" "$value"); else args+=("$flag"); fi
256
+ if oracle "${args[@]}" >/dev/null 2>"$key.err"; then
257
+ printf 'yes' > "$key"
258
+ return 0
259
+ fi
260
+ if grep -qiE 'unknown option|unknown argument|invalid option' "$key.err" 2>/dev/null; then
261
+ printf 'no' > "$key"
262
+ return 1
263
+ fi
264
+ # Conservative: if dry-run failed for some non-parse reason, do not use the optional flag.
265
+ printf 'no' > "$key"
266
+ return 1
267
+ }
268
+
269
+ add_supported_flag() {
270
+ # $1 is the target array name (historically a nameref); all callers use ARGS,
271
+ # so append to ARGS directly to stay compatible with Bash 3.2 (no `local -n`).
272
+ local flag="$2"
273
+ local value="${3-}"
274
+ if supports_option "$flag" "$value"; then
275
+ if [[ -n "$value" ]]; then ARGS+=("$flag" "$value"); else ARGS+=("$flag"); fi
276
+ else
277
+ warn "installed oracle does not accept $flag; skipping"
278
+ fi
279
+ }
280
+
281
+ # Auto context: small, local, non-secret evidence that helps ChatGPT plan/review without dumping the repo.
282
+ AUTO_FILES=()
283
+ write_context_file() {
284
+ local name="$1"
285
+ local content_cmd="$2"
286
+ local path="$CTX_DIR/$SLUG.$name"
287
+ bash -lc "$content_cmd" > "$path" 2>/dev/null || true
288
+ if [[ -s "$path" ]]; then AUTO_FILES+=("$path"); fi
289
+ }
290
+
291
+ if [[ "$AUTO_CONTEXT" -eq 1 ]]; then
292
+ # Authority class B — current policy/context (safe as source-of-truth).
293
+ policy_files=(CLAUDE.md composer.config.json docs/STATUS.md)
294
+ # Authority class C — background/history (risky as source-of-truth; only for planning/research).
295
+ background_files=(README.md AGENTS.md)
296
+ # Project manifest files (dependency intent + language).
297
+ base_files=(package.json pyproject.toml Cargo.toml go.mod)
298
+
299
+ # Task-aware attach set: minimal for trivial modes, no stale background for review/debug.
300
+ attach_candidates=()
301
+ case "$MODE" in
302
+ quick|standard)
303
+ attach_candidates=(CLAUDE.md docs/STATUS.md)
304
+ ;;
305
+ review|debug)
306
+ attach_candidates=("${policy_files[@]}" "${base_files[@]}")
307
+ ;;
308
+ deep|plan|research|*)
309
+ attach_candidates=("${policy_files[@]}" "${background_files[@]}" "${base_files[@]}")
310
+ ;;
311
+ esac
312
+
313
+ for candidate in "${attach_candidates[@]}"; do
314
+ [[ -f "$candidate" ]] && AUTO_FILES+=("$candidate")
315
+ done
316
+
317
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
318
+ write_context_file "git-status.txt" "git status --short"
319
+ write_context_file "git-diff-stat.txt" "git diff --stat"
320
+ if [[ "$MODE" != "quick" && "$MODE" != "standard" ]]; then
321
+ write_context_file "git-diff.patch" "git diff -- . ':(exclude).env' ':(exclude).env.*' ':(exclude)*.pem' ':(exclude)*.key' ':(exclude)*.p12' | 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' | head -c 200000"
322
+ fi
323
+ # Exact installed top-level deps (package.json shows ranges, not the installed tree).
324
+ if [[ "$MODE" != "quick" && "$MODE" != "standard" ]] && [[ -f package.json ]]; then
325
+ write_context_file "deps.txt" "npm ls --depth=0 2>/dev/null || true"
326
+ fi
327
+ fi
328
+ fi
329
+
330
+ ARGS=(--engine browser -m "$MODEL" --write-output "$OUT_FILE")
331
+
332
+ if [[ "$DRY_RUN" -eq 1 ]]; then
333
+ ARGS+=(--dry-run summary)
334
+ fi
335
+
336
+ # Official v0.13.0 accepts these, but many are hidden from --help; still probe to survive forks/older installs.
337
+ add_supported_flag ARGS --browser-model-strategy "${ORACLE_PRO_BROWSER_STRATEGY:-$DEFAULT_STRATEGY}"
338
+
339
+ if [[ "$USE_THINKING_FLAG" -eq 1 ]]; then
340
+ add_supported_flag ARGS --browser-thinking-time "$THINKING"
341
+ fi
342
+
343
+ if [[ "$RESEARCH_MODE" == "deep" ]]; then
344
+ add_supported_flag ARGS --browser-research deep
345
+ fi
346
+
347
+ if [[ "$USE_MANUAL_LOGIN" -eq 1 ]]; then
348
+ add_supported_flag ARGS --browser-manual-login
349
+ fi
350
+
351
+ add_supported_flag ARGS --browser-timeout "${ORACLE_PRO_TIMEOUT:-20m}"
352
+ add_supported_flag ARGS --browser-input-timeout "${ORACLE_PRO_INPUT_TIMEOUT:-60s}"
353
+ add_supported_flag ARGS --browser-attachments "$ATTACHMENTS_MODE"
354
+ add_supported_flag ARGS --browser-auto-reattach-delay "${ORACLE_PRO_REATTACH_DELAY:-30s}"
355
+ add_supported_flag ARGS --browser-auto-reattach-interval "${ORACLE_PRO_REATTACH_INTERVAL:-2m}"
356
+ add_supported_flag ARGS --browser-auto-reattach-timeout "${ORACLE_PRO_REATTACH_TIMEOUT:-2m}"
357
+ add_supported_flag ARGS --heartbeat "${ORACLE_PRO_HEARTBEAT:-30}"
358
+
359
+ # Validate + collect the exact attachment set (user files first, then auto context).
360
+ ATTACH=()
361
+ for f in "${FILES[@]}"; do
362
+ [[ -n "$f" ]] || continue
363
+ if is_secret_file "$f" && [[ "${ORACLE_PRO_ALLOW_SECRET_FILES:-0}" != "1" ]]; then
364
+ die "refusing to upload potential secret file: $f (matches secret denylist). Rename/relocate it, or set ORACLE_PRO_ALLOW_SECRET_FILES=1 to override."
365
+ fi
366
+ ATTACH+=("$f")
367
+ done
368
+ for f in "${AUTO_FILES[@]}"; do
369
+ [[ -n "$f" ]] && ATTACH+=("$f")
370
+ done
371
+
372
+ # Snapshot manifest: authoritative identity of the repo state this call may discuss.
373
+ # Lets the model (and us) detect drift between turns and bounds it to current disk state.
374
+ captured_at="$(date +%Y-%m-%dT%H:%M:%S%z)"
375
+ repo_root="$(pwd)"
376
+ git_branch="n/a"; git_head="n/a"; dirty="false"; status_hash="n/a"; diff_hash="n/a"
377
+ if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
378
+ git_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo n/a)"
379
+ git_head="$(git rev-parse HEAD 2>/dev/null || echo n/a)"
380
+ [[ -n "$(git status --porcelain 2>/dev/null)" ]] && dirty="true"
381
+ status_hash="$(git status --short 2>/dev/null | shasum -a 256 2>/dev/null | awk '{print $1}')"
382
+ diff_hash="$(git diff 2>/dev/null | shasum -a 256 2>/dev/null | awk '{print $1}')"
383
+ fi
384
+ [[ -n "$status_hash" ]] || status_hash="n/a"
385
+ [[ -n "$diff_hash" ]] || diff_hash="n/a"
386
+ node_ver="$(node --version 2>/dev/null || echo n/a)"
387
+ npm_ver="$(npm --version 2>/dev/null || echo n/a)"
388
+ os_ver="$(uname -sr 2>/dev/null || echo n/a)"
389
+ arch_ver="$(uname -m 2>/dev/null || echo n/a)"
390
+ short_head="${git_head:0:12}"
391
+ repo_state_hash="$(printf '%s' "${git_head}${status_hash}${diff_hash}${node_ver}${npm_ver}" | shasum -a 256 2>/dev/null | awk '{print $1}')"
392
+ [[ -n "$repo_state_hash" ]] || repo_state_hash="n/a"
393
+ snapshot_id="${SLUG}-${short_head}-${repo_state_hash:0:8}"
394
+
395
+ MANIFEST_PATH="$CTX_DIR/$SLUG.manifest.json"
396
+ # JSON-escape a string value (backslash and double-quote only; inputs are paths/hashes/versions).
397
+ json_escape() { printf '%s' "${1-}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
398
+ {
399
+ printf '{\n'
400
+ printf ' "snapshotId": "%s",\n' "$(json_escape "$snapshot_id")"
401
+ printf ' "capturedAt": "%s",\n' "$(json_escape "$captured_at")"
402
+ printf ' "repoRoot": "%s",\n' "$(json_escape "$repo_root")"
403
+ printf ' "branch": "%s",\n' "$(json_escape "$git_branch")"
404
+ printf ' "head": "%s",\n' "$(json_escape "$git_head")"
405
+ printf ' "dirty": %s,\n' "$dirty"
406
+ printf ' "gitStatusHash": "%s",\n' "$(json_escape "$status_hash")"
407
+ printf ' "diffHash": "%s",\n' "$(json_escape "$diff_hash")"
408
+ printf ' "repoStateHash": "%s",\n' "$(json_escape "$repo_state_hash")"
409
+ printf ' "mode": "%s",\n' "$(json_escape "$MODE")"
410
+ printf ' "runtime": { "node": "%s", "npm": "%s", "os": "%s", "arch": "%s" },\n' \
411
+ "$(json_escape "$node_ver")" "$(json_escape "$npm_ver")" "$(json_escape "$os_ver")" "$(json_escape "$arch_ver")"
412
+ printf ' "attachments": [\n'
413
+ manifest_n=${#ATTACH[@]}
414
+ manifest_i=0
415
+ for f in "${ATTACH[@]}"; do
416
+ manifest_i=$((manifest_i + 1))
417
+ sha="$(shasum -a 256 "$f" 2>/dev/null | awk '{print $1}')"
418
+ bytes="$(wc -c < "$f" 2>/dev/null | tr -d ' ')"
419
+ sep=","
420
+ [[ "$manifest_i" -eq "$manifest_n" ]] && sep=""
421
+ printf ' { "path": "%s", "sha256": "%s", "bytes": %s }%s\n' \
422
+ "$(json_escape "$f")" "${sha:-}" "${bytes:-0}" "$sep"
423
+ done
424
+ printf ' ]\n'
425
+ printf '}\n'
426
+ } > "$MANIFEST_PATH" 2>/dev/null || warn "manifest generation failed (continuing without manifest)"
427
+ [[ -f "$MANIFEST_PATH" ]] && ATTACH+=("$MANIFEST_PATH")
428
+
429
+ # CONTEXT CONTRACT: bound the model to this snapshot; defeat stale memory/attachments.
430
+ CONTRACT="$(cat <<EOF
431
+ CONTEXT CONTRACT
432
+ Authoritative snapshot: ${snapshot_id}
433
+ Captured at: ${captured_at}
434
+ Branch / HEAD: ${git_branch} / ${git_head}
435
+ Dirty tree: ${dirty}
436
+ Repo-state hash: ${repo_state_hash}
437
+ Runtime: node=${node_ver} npm=${npm_ver} os=${os_ver} arch=${arch_ver}
438
+ Task: ${MODE}
439
+ Authority order:
440
+ A. Live source-of-truth: ${SLUG}.manifest.json, ${SLUG}.git-status.txt, ${SLUG}.git-diff.patch, ${SLUG}.deps.txt, targeted source/tests
441
+ B. Current policy/context: CLAUDE.md, composer.config.json, docs/STATUS.md, relevant ADRs
442
+ C. Background/history: README.md, AGENTS.md
443
+ Rules:
444
+ - Treat class A as authoritative for the local repo. If A conflicts with B or C, A wins.
445
+ - Ignore prior chat memory, project memory, and earlier attachments if they conflict with this snapshot.
446
+ - For each substantive claim, tag it [attached], [runtime], [web], or [inference].
447
+ - Cite attached claims with file path and line span.
448
+ - For current API/library claims not proven by attached files, verify on the web against primary docs.
449
+ - If evidence is insufficient, say: "unknown from provided context".
450
+ EOF
451
+ )"
452
+ PROMPT="${CONTRACT}
453
+
454
+ ${PROMPT}"
455
+
456
+ # Emit all attachments (incl. the manifest) as --file inputs.
457
+ for f in "${ATTACH[@]}"; do
458
+ [[ -n "$f" ]] && ARGS+=(--file "$f")
459
+ done
460
+
461
+ log "oracle version: ${ORACLE_VERSION:-unknown}"
462
+ log "mode=$MODE model=$MODEL thinking=$THINKING research=$RESEARCH_MODE output=$OUT_FILE"
463
+ log "files: user=${#FILES[@]} auto=${#AUTO_FILES[@]} total=${#ATTACH[@]} snapshot=${snapshot_id}"
464
+
465
+ oracle "${ARGS[@]}" -p "$PROMPT"
466
+
467
+ # Maintain a stable latest file for downstream Codex/Composer commands.
468
+ if [[ -f "$OUT_FILE" ]]; then
469
+ cp "$OUT_FILE" "$OUT_DIR/latest.md"
470
+ printf '%s\n' "$OUT_FILE"
471
+ fi