forgedock 1.1.10 → 1.2.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.
- package/README.md +1 -1
- package/bin/cli-spawn-shared.mjs +25 -0
- package/bin/engine-cli.mjs +22 -1
- package/bin/engine.mjs +46 -4
- package/bin/forgedock.mjs +166 -20
- package/bin/hooks/pre-tool-use.mjs +331 -10
- package/bin/init-detect.mjs +1 -1
- package/bin/init-enrich-api.mjs +12 -4
- package/bin/init-enrich-cli.mjs +328 -0
- package/bin/init-enrich.mjs +137 -0
- package/bin/journey.mjs +328 -50
- package/bin/runner.mjs +653 -10
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +10 -7
- package/commands/audit-agents.md +29 -6
- package/commands/audit.md +9 -20
- package/commands/autopilot.md +52 -78
- package/commands/changelog.md +1 -1
- package/commands/ci-audit.md +19 -10
- package/commands/cleanup.md +1 -1
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/design-bench.md +1 -1
- package/commands/design-render-critique-loop.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/forge-stats.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +9 -6
- package/commands/incident-response.md +15 -5
- package/commands/issue.md +298 -15
- package/commands/milestone.md +45 -18
- package/commands/optimize.md +1 -1
- package/commands/orchestrate/phase-1-resolve.md +20 -5
- package/commands/orchestrate/phase-4-execution.md +6 -1
- package/commands/orchestrate/phase-5-cleanup.md +43 -2
- package/commands/orchestrate/phase-6-report.md +1 -0
- package/commands/orchestrate.md +1 -1
- package/commands/pipeline-health/phase-4-proposals.md +40 -10
- package/commands/pipeline-health/phase-5-report.md +140 -13
- package/commands/pipeline-health.md +1 -1
- package/commands/pipeline-resume.md +1 -1
- package/commands/pipeline-status.md +1 -1
- package/commands/qa-sweep.md +10 -4
- package/commands/recover-orphans.md +1 -1
- package/commands/review-pr-agents/protocols.md +4 -0
- package/commands/review-pr-agents.md +7 -4
- package/commands/review-pr-staging.md +44 -15
- package/commands/review-pr.md +73 -28
- package/commands/rollback.md +15 -5
- package/commands/security-audit.md +14 -11
- package/commands/signal-planner.md +32 -12
- package/commands/spec-doctor.md +1 -1
- package/commands/sync-ecosystem.md +54 -11
- package/commands/test-gate.md +49 -11
- package/commands/validate.md +28 -9
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/context.md +1 -1
- package/commands/work-on/build/implement.md +2 -2
- package/commands/work-on/build/validate.md +3 -1
- package/commands/work-on/build.md +1 -1
- package/commands/work-on/close.md +1 -1
- package/commands/work-on/decompose.md +27 -24
- package/commands/work-on/investigate.md +2 -2
- package/commands/work-on/remediate.md +11 -3
- package/commands/work-on/review.md +5 -3
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +38 -8
- package/docs/CI.md +18 -0
- package/docs/CONFIG.md +5 -2
- package/docs/site/annotations-explained.md +2 -2
- package/docs/site/command-reference.md +21 -0
- package/docs/spec/review-protocol.md +6 -0
- package/package.json +1 -1
- package/scripts/danger-zones.mjs +3 -1
- package/scripts/transition-label.sh +80 -28
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-enrich-cli.mjs — local Claude Code CLI enrichment backend for
|
|
3
|
+
* ForgeDock init (issue #2004).
|
|
4
|
+
*
|
|
5
|
+
* Implements the same enrich(ConfigDraft) contract as the API backend
|
|
6
|
+
* (bin/init-enrich-api.mjs) so the selection ladder in bin/init-enrich.mjs
|
|
7
|
+
* can treat both backends interchangeably. Unlike the API backend, this
|
|
8
|
+
* backend requires NO ANTHROPIC_API_KEY — it shells out to an already
|
|
9
|
+
* authenticated local `claude` CLI (subscription OAuth or CLI-managed key)
|
|
10
|
+
* in headless print mode.
|
|
11
|
+
*
|
|
12
|
+
* SECURITY — argv array, NEVER `shell: true`: the enrichment prompt embeds
|
|
13
|
+
* the full ConfigDraft JSON as a single, unparsed argv element passed to
|
|
14
|
+
* `spawnSync(bin, [...argv])` with no shell. This mirrors the hardened
|
|
15
|
+
* pattern in bin/runner.mjs's runCliBackend (issue #2003), fixed in commit
|
|
16
|
+
* cf742a4 after an earlier shell-injection finding: an earlier version of
|
|
17
|
+
* that function built a shell command *string* run via
|
|
18
|
+
* `spawnSync(command, { shell: true })` with hand-rolled quoting that did
|
|
19
|
+
* NOT neutralize `$(...)`/backtick/`!` expansion inside double quotes. Do
|
|
20
|
+
* not reintroduce `shell: true` or string-command interpolation here.
|
|
21
|
+
*
|
|
22
|
+
* SECURITY — read-only enforcement via --allowedTools/--disallowedTools, NOT
|
|
23
|
+
* --dangerously-skip-permissions: the prompt tells the model it "MUST NOT
|
|
24
|
+
* modify, create, or delete any files," but `forgedock init` runs against an
|
|
25
|
+
* arbitrary user's repo — untrusted content (README, existing config, file
|
|
26
|
+
* names) could carry a prompt-injection payload that induces writes. A prose
|
|
27
|
+
* instruction alone is not enforcement. `--dangerously-skip-permissions`
|
|
28
|
+
* bypasses ALL permission checks for ALL tools (Write, Edit, Bash,
|
|
29
|
+
* NotebookEdit, etc.), so it cannot be used here. Instead the session is
|
|
30
|
+
* restricted with `--allowedTools` (pre-approved, read-only tools) and
|
|
31
|
+
* `--disallowedTools` (explicitly denied mutating tools) — both are
|
|
32
|
+
* headless-safe (no interactive TTY prompt is generated for tools covered by
|
|
33
|
+
* either list), so this preserves the original reason
|
|
34
|
+
* `--dangerously-skip-permissions` was used (avoiding a hang on an
|
|
35
|
+
* unanswerable permission prompt in `--print` mode) while making the
|
|
36
|
+
* read-only contract technically enforced instead of compliance-only. Do not
|
|
37
|
+
* reintroduce `--dangerously-skip-permissions` here. (See issue #2022.)
|
|
38
|
+
*
|
|
39
|
+
* The allow-list is the primary enforcement boundary, NOT the disallow-list:
|
|
40
|
+
* only the tools named in `--allowedTools` ("Read Glob Grep LS") are
|
|
41
|
+
* permitted to execute at all — everything else is implicitly denied by
|
|
42
|
+
* omission, exactly like a default-deny firewall rule. `--disallowedTools`
|
|
43
|
+
* ("Write Edit NotebookEdit Bash") is passed in addition purely as
|
|
44
|
+
* defense-in-depth: an explicit, redundant belt-and-suspenders denial of the
|
|
45
|
+
* specific mutating tools most likely to be added to a future CLI default
|
|
46
|
+
* allow-set. The two lists currently have zero overlap (no tool appears in
|
|
47
|
+
* both), so no conflict exists today — do not add an overlapping entry
|
|
48
|
+
* without first confirming the CLI's actual conflict-resolution behavior
|
|
49
|
+
* (which list wins is CLI-version-dependent and not verified here). What
|
|
50
|
+
* matters for THIS module's security contract is simpler and does not
|
|
51
|
+
* depend on that resolution order: keep `--allowedTools` scoped to the
|
|
52
|
+
* minimal read-only set, since that is what makes every tool this session
|
|
53
|
+
* can possibly invoke enumerable in one place. Do not read the
|
|
54
|
+
* disallow-list as the primary boundary and the allow-list as merely
|
|
55
|
+
* advisory; the reverse is true. (See issue #2047.)
|
|
56
|
+
*
|
|
57
|
+
* Windows `.cmd` shim resolution works WITHOUT `shell: true`: Node's
|
|
58
|
+
* spawn/spawnSync resolve a bare `"claude"` command via PATH+PATHEXT and
|
|
59
|
+
* safely re-invoke through cmd.exe internally when the resolved target is a
|
|
60
|
+
* `.cmd`/`.bat` file — argv elements are never parsed as shell syntax. Do
|
|
61
|
+
* NOT use `execFileSync("claude", [...])` here; it does not resolve `.cmd`
|
|
62
|
+
* shims on Windows and throws ENOENT even when the CLI is genuinely
|
|
63
|
+
* installed (see issue #394/#382 — this exact regression previously made an
|
|
64
|
+
* unrelated skill-based enrichment backend silently dead on Windows).
|
|
65
|
+
*
|
|
66
|
+
* Graceful fallback: any failure (CLI absent, timeout, non-zero exit, spawn
|
|
67
|
+
* error, malformed/unparseable output) returns the original draft unchanged
|
|
68
|
+
* and never throws — mirrors bin/init-enrich-api.mjs's existing contract so
|
|
69
|
+
* a backend failure can never crash `forgedock init`. Failure category is
|
|
70
|
+
* always surfaced to stderr (unconditionally, not gated behind
|
|
71
|
+
* FORGEDOCK_DEBUG) — mirrors the API backend's error-surfacing discipline
|
|
72
|
+
* (see issues #491/#497); only the raw error detail is debug-gated.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
import { spawnSync } from "child_process";
|
|
76
|
+
import { dim, yellow, RESET } from "./tui.mjs";
|
|
77
|
+
import { parseEnrichedDraft } from "./init-enrich-api.mjs";
|
|
78
|
+
import { DEFAULT_SPAWN_MAX_BUFFER_BYTES } from "./cli-spawn-shared.mjs";
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Default wall-clock limit for a single `claude --print` enrichment call.
|
|
82
|
+
* This is a single-shot headless prompt (not a multi-turn agent loop like
|
|
83
|
+
* bin/runner.mjs's runCliBackend), so it is deliberately much shorter than
|
|
84
|
+
* that function's 15-minute DEFAULT_CLI_TIMEOUT_MS. Override via
|
|
85
|
+
* FORGEDOCK_CLI_ENRICH_TIMEOUT_MS (ms).
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_ENRICH_TIMEOUT_MS = 2 * 60 * 1000;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Default maximum size (bytes, UTF-8) for the built enrichment prompt
|
|
91
|
+
* (which embeds the full ConfigDraft JSON as a single argv element) before
|
|
92
|
+
* `enrich()` skips the CLI spawn entirely and falls back to the baseline
|
|
93
|
+
* draft unchanged (issue #2016).
|
|
94
|
+
*
|
|
95
|
+
* argv passed to `spawnSync` with `shell: false` (this module's contract —
|
|
96
|
+
* see the SECURITY note above) still counts against the OS's total argv+env
|
|
97
|
+
* size limit (`ARG_MAX` on POSIX, ~2MB+ — not the binding constraint here;
|
|
98
|
+
* ~32K *characters* for a single CreateProcess command line on Windows IS
|
|
99
|
+
* the binding constraint — see the SYSTEM PROMPT note in bin/runner.mjs's
|
|
100
|
+
* runCliBackend for the same constraint on a different code path, where it
|
|
101
|
+
* is avoided entirely by writing the payload to a temp file instead of
|
|
102
|
+
* argv). A ConfigDraft is normally a few KB, but pathological inputs (very
|
|
103
|
+
* large `review.tech_stack`/`repos.satellites` arrays from a
|
|
104
|
+
* multi-hundred-repo org) could grow large enough to risk hitting that OS
|
|
105
|
+
* ceiling — which would surface as an opaque spawn failure rather than a
|
|
106
|
+
* clean, diagnosable fallback. Unlike runCliBackend, this prompt is always
|
|
107
|
+
* passed inline (no temp-file indirection), so the guard must actually sit
|
|
108
|
+
* BELOW the Windows ~32K character ceiling — not merely "below every
|
|
109
|
+
* relevant platform's limit" in the abstract, since Windows is the tightest
|
|
110
|
+
* one. 24 KB leaves comfortable headroom for the handful of other short
|
|
111
|
+
* argv elements (`--print`, `--allowedTools`, the tool-name strings,
|
|
112
|
+
* `--disallowedTools`) that share the same command-line budget, while still
|
|
113
|
+
* being far above any realistic ConfigDraft size. Override via
|
|
114
|
+
* FORGEDOCK_CLI_ENRICH_MAX_PROMPT_BYTES.
|
|
115
|
+
*/
|
|
116
|
+
const DEFAULT_ENRICH_MAX_PROMPT_BYTES = 24 * 1024;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build the headless enrichment prompt sent to the local `claude` CLI.
|
|
120
|
+
*
|
|
121
|
+
* Unlike the API backend's SYSTEM_PROMPT (which explicitly tells the model
|
|
122
|
+
* it has NO filesystem/tool access, since it's a stateless Messages API
|
|
123
|
+
* call), the CLI backend genuinely runs with the CLI's normal tool access —
|
|
124
|
+
* that access is the whole value proposition of this backend over the
|
|
125
|
+
* API backend (it can inspect the actual repo, not just the ConfigDraft
|
|
126
|
+
* JSON). The prompt still constrains scope and output shape identically to
|
|
127
|
+
* the API backend's contract so downstream consumers (parseEnrichedDraft,
|
|
128
|
+
* forge.yaml writer) see a consistent shape regardless of backend.
|
|
129
|
+
*
|
|
130
|
+
* @param {object} draft - ConfigDraft from detectConfig()
|
|
131
|
+
* @returns {string}
|
|
132
|
+
*/
|
|
133
|
+
function buildEnrichPrompt(draft) {
|
|
134
|
+
const draftJson = JSON.stringify(draft, null, 2);
|
|
135
|
+
return (
|
|
136
|
+
"You are the init-enrich CLI backend for ForgeDock. You receive a ConfigDraft " +
|
|
137
|
+
"JSON object and return an enriched version, following the enrich(ConfigDraft) " +
|
|
138
|
+
"contract shared with the API enrichment backend.\n\n" +
|
|
139
|
+
"ENRICHMENT SCOPE (what you MAY improve):\n" +
|
|
140
|
+
"- review.tech_stack: infer from the current working directory's codebase if recognizable patterns appear.\n" +
|
|
141
|
+
"- review.context: improve wording or add standard context based on the project.\n" +
|
|
142
|
+
"- verification.health_patterns: suggest standard patterns for known frameworks.\n" +
|
|
143
|
+
"You may use your available tools to inspect the current working directory " +
|
|
144
|
+
"(read files, list directories) to improve accuracy, but MUST NOT modify, " +
|
|
145
|
+
"create, or delete any files.\n\n" +
|
|
146
|
+
"STRICTLY PROHIBITED — set confidence to 'low' and do NOT invent values for:\n" +
|
|
147
|
+
"- project_board.project_id (must match ^PVT_[A-Za-z0-9_=-]+$ — you cannot verify these).\n" +
|
|
148
|
+
"- project_board.field_ids.* (PVTSSF_ strings — you cannot verify these).\n" +
|
|
149
|
+
"- project_board.project_number (you cannot query the GitHub Projects API without gh CLI access to the correct account).\n" +
|
|
150
|
+
"- repos.satellites (you cannot verify satellite repositories exist).\n" +
|
|
151
|
+
"Never invent a PVT_ or PVTSSF_ string. If you cannot verify a field, set confidence to 'low'.\n\n" +
|
|
152
|
+
"OUTPUT RULES:\n" +
|
|
153
|
+
"- Return ONLY the enriched ConfigDraft as a valid JSON object.\n" +
|
|
154
|
+
"- Every leaf must have shape { value, confidence, source, why }.\n" +
|
|
155
|
+
"- Do not modify project, paths, branches, or meta sections.\n" +
|
|
156
|
+
"- Output the JSON object alone with no surrounding prose, no markdown code fences.\n\n" +
|
|
157
|
+
"ConfigDraft:\n" +
|
|
158
|
+
draftJson
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Enrich a ConfigDraft by invoking the local Claude Code CLI in headless
|
|
164
|
+
* print mode. This is the cli backend for the init-enrich interface — it
|
|
165
|
+
* implements the same enrich(ConfigDraft) contract as the api backend
|
|
166
|
+
* (bin/init-enrich-api.mjs) so the selection ladder (bin/init-enrich.mjs)
|
|
167
|
+
* can treat both interchangeably.
|
|
168
|
+
*
|
|
169
|
+
* Callers should verify CLI availability (via isClaudeCliAvailable from
|
|
170
|
+
* bin/runner.mjs) before calling this — this function does not probe
|
|
171
|
+
* availability itself, it directly attempts invocation and falls back
|
|
172
|
+
* gracefully if the CLI turns out to be unavailable or misbehaves.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} draft - ConfigDraft from detectConfig()
|
|
175
|
+
* @param {object} [opts]
|
|
176
|
+
* @param {string} [opts.cwd] - Working directory for the CLI invocation
|
|
177
|
+
* (lets the CLI inspect the actual repo being configured). Defaults to
|
|
178
|
+
* process.cwd().
|
|
179
|
+
* @param {string} [opts.bin] - Executable to invoke (default "claude").
|
|
180
|
+
* Test seam only — production callers must never override this.
|
|
181
|
+
* @param {number} [opts.timeoutMs] - Wall-clock timeout override. Defaults
|
|
182
|
+
* to FORGEDOCK_CLI_ENRICH_TIMEOUT_MS env var, else DEFAULT_ENRICH_TIMEOUT_MS.
|
|
183
|
+
* @param {Function} [opts.spawnFn] - Injectable replacement for
|
|
184
|
+
* child_process.spawnSync. Test seam only — lets tests deterministically
|
|
185
|
+
* simulate success/timeout/non-zero-exit/spawn-error without depending on
|
|
186
|
+
* a real `claude` install or OS-specific executable shimming.
|
|
187
|
+
* @param {object} [opts.env] - Environment to derive the scrubbed child env
|
|
188
|
+
* from (see SECURITY note below). Defaults to process.env. Test seam —
|
|
189
|
+
* mirrors bin/init-enrich-api.mjs's opts.env, lets tests assert on the
|
|
190
|
+
* scrub without mutating the real process env.
|
|
191
|
+
* @returns {Promise<object>} Enriched ConfigDraft, or the original draft on
|
|
192
|
+
* any failure.
|
|
193
|
+
*/
|
|
194
|
+
export async function enrich(draft, opts = {}) {
|
|
195
|
+
const {
|
|
196
|
+
cwd = process.cwd(),
|
|
197
|
+
bin = "claude",
|
|
198
|
+
timeoutMs,
|
|
199
|
+
spawnFn = spawnSync,
|
|
200
|
+
env = process.env,
|
|
201
|
+
} = opts;
|
|
202
|
+
|
|
203
|
+
const rawTimeout = parseInt(process.env.FORGEDOCK_CLI_ENRICH_TIMEOUT_MS, 10);
|
|
204
|
+
const resolvedTimeoutMs =
|
|
205
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
206
|
+
? timeoutMs
|
|
207
|
+
: Number.isFinite(rawTimeout) && rawTimeout > 0
|
|
208
|
+
? rawTimeout
|
|
209
|
+
: DEFAULT_ENRICH_TIMEOUT_MS;
|
|
210
|
+
|
|
211
|
+
const message = buildEnrichPrompt(draft);
|
|
212
|
+
|
|
213
|
+
// Reject an oversized prompt before ever spawning — see the
|
|
214
|
+
// DEFAULT_ENRICH_MAX_PROMPT_BYTES doc comment (issue #2016). This mirrors
|
|
215
|
+
// the graceful-fallback contract of every other failure branch below: no
|
|
216
|
+
// throw, just an early return of the original draft.
|
|
217
|
+
const rawMaxPromptBytes = parseInt(
|
|
218
|
+
process.env.FORGEDOCK_CLI_ENRICH_MAX_PROMPT_BYTES,
|
|
219
|
+
10,
|
|
220
|
+
);
|
|
221
|
+
const maxPromptBytes =
|
|
222
|
+
Number.isFinite(rawMaxPromptBytes) && rawMaxPromptBytes > 0
|
|
223
|
+
? rawMaxPromptBytes
|
|
224
|
+
: DEFAULT_ENRICH_MAX_PROMPT_BYTES;
|
|
225
|
+
const messageBytes = Buffer.byteLength(message, "utf-8");
|
|
226
|
+
if (messageBytes > maxPromptBytes) {
|
|
227
|
+
warn(
|
|
228
|
+
`skipped: ConfigDraft is too large to pass as a CLI argument (${messageBytes} bytes exceeds the ${maxPromptBytes} byte limit) — falling back to baseline draft`,
|
|
229
|
+
);
|
|
230
|
+
return draft;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Scrub the Anthropic API key from the child environment. Even though this
|
|
234
|
+
// backend is restricted to read-only tools (no bash) via
|
|
235
|
+
// --allowedTools/--disallowedTools, the spawned `claude` process would
|
|
236
|
+
// otherwise still inherit ANTHROPIC_API_KEY unnecessarily — mirrors the
|
|
237
|
+
// scrub in bin/runner.mjs's runCliBackend/run_bash. GH_TOKEN/GITHUB_TOKEN
|
|
238
|
+
// and all other env vars are intentionally left intact.
|
|
239
|
+
const childEnv = { ...env };
|
|
240
|
+
delete childEnv.ANTHROPIC_API_KEY;
|
|
241
|
+
|
|
242
|
+
let result;
|
|
243
|
+
try {
|
|
244
|
+
// No `shell` option (defaults to false): argv is passed as discrete,
|
|
245
|
+
// unparsed elements — see the SECURITY note in the module header. This
|
|
246
|
+
// also correctly resolves the Windows `.cmd` shim without shell:true.
|
|
247
|
+
//
|
|
248
|
+
// --allowedTools/--disallowedTools (NOT --dangerously-skip-permissions):
|
|
249
|
+
// technically restricts this headless session to read-only tools so the
|
|
250
|
+
// prompt's "MUST NOT modify, create, or delete any files" instruction is
|
|
251
|
+
// CLI-enforced, not just prose — see the SECURITY note in the module
|
|
252
|
+
// header (issue #2022).
|
|
253
|
+
result = spawnFn(
|
|
254
|
+
bin,
|
|
255
|
+
[
|
|
256
|
+
"--print",
|
|
257
|
+
message,
|
|
258
|
+
"--allowedTools",
|
|
259
|
+
"Read Glob Grep LS",
|
|
260
|
+
"--disallowedTools",
|
|
261
|
+
"Write Edit NotebookEdit Bash",
|
|
262
|
+
],
|
|
263
|
+
{
|
|
264
|
+
cwd,
|
|
265
|
+
encoding: "utf-8",
|
|
266
|
+
maxBuffer: DEFAULT_SPAWN_MAX_BUFFER_BYTES,
|
|
267
|
+
timeout: resolvedTimeoutMs,
|
|
268
|
+
env: childEnv,
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
// Unconditional summary is intentionally generic — err.message can
|
|
273
|
+
// contain local filesystem paths (e.g. an ENOENT spawn error against a
|
|
274
|
+
// custom `bin` path), which must not appear in always-visible stderr
|
|
275
|
+
// output (issue #2017). Full detail is still available via the
|
|
276
|
+
// FORGEDOCK_DEBUG-gated line inside warn().
|
|
277
|
+
warn("unavailable (invocation failed)", err);
|
|
278
|
+
return draft;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!result) {
|
|
282
|
+
warn("unavailable: no result from CLI invocation");
|
|
283
|
+
return draft;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const timedOut = result.status === null && result.error?.code === "ETIMEDOUT";
|
|
287
|
+
if (timedOut) {
|
|
288
|
+
const timeoutSecs = Math.round(resolvedTimeoutMs / 1000);
|
|
289
|
+
warn(
|
|
290
|
+
`timed out after ${timeoutSecs}s and was killed — falling back to baseline draft`,
|
|
291
|
+
);
|
|
292
|
+
return draft;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (result.error) {
|
|
296
|
+
// Same rationale as the catch block above: keep the unconditional
|
|
297
|
+
// summary generic, raw detail (which can include local paths) stays
|
|
298
|
+
// behind FORGEDOCK_DEBUG (issue #2017).
|
|
299
|
+
warn("failed to invoke (spawn-level error)", result.error);
|
|
300
|
+
return draft;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (result.status !== 0) {
|
|
304
|
+
warn(`exited with status ${result.status ?? "?"} — falling back to baseline draft`);
|
|
305
|
+
return draft;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const stdout = result.stdout ? String(result.stdout) : "";
|
|
309
|
+
return parseEnrichedDraft(stdout, draft);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Emit a two-tier warning: a short, always-visible category to stderr
|
|
314
|
+
* (never gated behind FORGEDOCK_DEBUG — see issues #491/#497 for why silent
|
|
315
|
+
* failures are a bug class here), plus the raw error detail only under
|
|
316
|
+
* FORGEDOCK_DEBUG.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} summary - Short, user-facing failure category.
|
|
319
|
+
* @param {Error} [err] - Original error, logged only under FORGEDOCK_DEBUG.
|
|
320
|
+
*/
|
|
321
|
+
function warn(summary, err) {
|
|
322
|
+
console.error(
|
|
323
|
+
` ${yellow("[!]")} CLI enrichment ${summary}.${RESET}`,
|
|
324
|
+
);
|
|
325
|
+
if (process.env.FORGEDOCK_DEBUG && err) {
|
|
326
|
+
console.error(` ${dim("[debug]")} cli enrichment failed: ${err.message}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init-enrich.mjs — enrichment backend selection ladder for ForgeDock init
|
|
3
|
+
* (issue #2004).
|
|
4
|
+
*
|
|
5
|
+
* `forgedock init`'s AI enrichment step has two interchangeable backends,
|
|
6
|
+
* both implementing the same enrich(ConfigDraft) contract:
|
|
7
|
+
* - cli: bin/init-enrich-cli.mjs — local Claude Code CLI, no API key needed.
|
|
8
|
+
* - api: bin/init-enrich-api.mjs — Anthropic Messages API, needs ANTHROPIC_API_KEY.
|
|
9
|
+
*
|
|
10
|
+
* Selection ladder (mirrors the "auto" ladder bin/runner.mjs established for
|
|
11
|
+
* the `forgedock run` engine backend in issue #2003, applied here to the
|
|
12
|
+
* separate init-enrichment call site):
|
|
13
|
+
* 1. `claude` CLI present and responding → cli backend. Reuses whatever
|
|
14
|
+
* the CLI is already authenticated with (Pro/Max OAuth or a
|
|
15
|
+
* CLI-managed key) — no ANTHROPIC_API_KEY required.
|
|
16
|
+
* 2. ANTHROPIC_API_KEY set → api backend (existing, unchanged behavior).
|
|
17
|
+
* 3. Neither → "none". Caller (bin/journey.mjs) skips enrichment and
|
|
18
|
+
* prints an explanatory message; the deterministic detection baseline
|
|
19
|
+
* is used unchanged.
|
|
20
|
+
*
|
|
21
|
+
* This is a separate module from bin/runner.mjs's own ladder — the two
|
|
22
|
+
* invocation shapes differ (a multi-turn agent tool-use loop for
|
|
23
|
+
* `forgedock run` vs. a one-shot `--print` enrichment prompt here), and the
|
|
24
|
+
* two ladders' override-validation and no-CLI fallback behavior are
|
|
25
|
+
* intentionally different (see resolveEnrichBackend()'s doc comment below).
|
|
26
|
+
* But the common "explicit override wins, else probe CLI, prefer cli" shape
|
|
27
|
+
* IS now shared: both this module's resolveEnrichBackend() and
|
|
28
|
+
* bin/runner.mjs's resolveBackend() delegate to the same
|
|
29
|
+
* resolveBackendLadder() primitive (issue #2026), each supplying its own
|
|
30
|
+
* override set and CLI-unavailable fallback. The CLI-presence probe itself
|
|
31
|
+
* (isClaudeCliAvailable) is reused directly from bin/runner.mjs with zero
|
|
32
|
+
* duplication, same as before.
|
|
33
|
+
*
|
|
34
|
+
* Override (issue #2023): the ladder above changed the *default* precedence
|
|
35
|
+
* from "API-key-only" to "CLI-first" (#2004). Existing users who have both
|
|
36
|
+
* ANTHROPIC_API_KEY set and a `claude` CLI on PATH get silently moved onto
|
|
37
|
+
* the cli backend with no way to opt back into the old, explicit api-key
|
|
38
|
+
* behavior. `FORGEDOCK_INIT_BACKEND` (values: "cli"|"api"|"none"|"auto")
|
|
39
|
+
* gives them that escape hatch, mirroring bin/runner.mjs's FORGEDOCK_BACKEND
|
|
40
|
+
* override for the `forgedock run` engine ladder (#2003). Unset, empty, or
|
|
41
|
+
* "auto" — including any unrecognized value — falls through to the ladder
|
|
42
|
+
* unchanged, so the #2004 default behavior is byte-for-byte preserved for
|
|
43
|
+
* anyone who doesn't set the var.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { isClaudeCliAvailable, resolveBackendLadder } from "./runner.mjs";
|
|
47
|
+
import { enrich as enrichViaApi } from "./init-enrich-api.mjs";
|
|
48
|
+
import { enrich as enrichViaCli } from "./init-enrich-cli.mjs";
|
|
49
|
+
|
|
50
|
+
/** Valid explicit values for FORGEDOCK_INIT_BACKEND (excluding "auto", which
|
|
51
|
+
* is the no-override default and falls through to the ladder below). */
|
|
52
|
+
const VALID_INIT_BACKEND_OVERRIDES = new Set(["cli", "api", "none"]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve which enrichment backend should be used.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} [opts]
|
|
58
|
+
* @param {string} [opts.cwd] - Working directory for the CLI probe. Defaults
|
|
59
|
+
* to process.cwd().
|
|
60
|
+
* @param {object} [opts.env] - Environment to read ANTHROPIC_API_KEY and
|
|
61
|
+
* FORGEDOCK_INIT_BACKEND from. Defaults to process.env. Injectable so
|
|
62
|
+
* callers (bin/journey.mjs) can pass ctx.env for testability.
|
|
63
|
+
* @param {Function} [opts.isCliAvailableFn] - Injectable replacement for
|
|
64
|
+
* isClaudeCliAvailable. Test seam — lets tests deterministically simulate
|
|
65
|
+
* CLI presence/absence without depending on whether the CI host happens
|
|
66
|
+
* to have `claude` on PATH.
|
|
67
|
+
* @returns {"cli"|"api"|"none"}
|
|
68
|
+
*/
|
|
69
|
+
export function resolveEnrichBackend({
|
|
70
|
+
cwd = process.cwd(),
|
|
71
|
+
env = process.env,
|
|
72
|
+
isCliAvailableFn = isClaudeCliAvailable,
|
|
73
|
+
} = {}) {
|
|
74
|
+
// Explicit override (#2023) — takes precedence over the auto ladder.
|
|
75
|
+
// "auto" and any unrecognized value intentionally fall through unchanged,
|
|
76
|
+
// never throw: this preserves the "graceful fallback, never crash init"
|
|
77
|
+
// discipline the rest of this module follows. This validation (silent
|
|
78
|
+
// fallthrough on an invalid value) is intentionally NOT shared with
|
|
79
|
+
// resolveBackend()'s throw-on-invalid behavior — see resolveBackendLadder()
|
|
80
|
+
// in bin/runner.mjs, which defers all override validation to callers.
|
|
81
|
+
const override = env.FORGEDOCK_INIT_BACKEND;
|
|
82
|
+
|
|
83
|
+
// Delegates the shared "override-or-probe-CLI" shape to
|
|
84
|
+
// resolveBackendLadder() (issue #2026). The CLI-unavailable fallback below
|
|
85
|
+
// preserves this module's own eager-inline ANTHROPIC_API_KEY check
|
|
86
|
+
// (distinct from resolveBackend()'s unconditional "api" fallback).
|
|
87
|
+
return resolveBackendLadder({
|
|
88
|
+
override,
|
|
89
|
+
validOverrides: VALID_INIT_BACKEND_OVERRIDES,
|
|
90
|
+
cwd,
|
|
91
|
+
isCliAvailableFn,
|
|
92
|
+
cliFallback: () => (env.ANTHROPIC_API_KEY ? "api" : "none"),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Enrich a ConfigDraft using whichever backend is available, per the
|
|
98
|
+
* selection ladder in resolveEnrichBackend(). Both backends implement the
|
|
99
|
+
* same contract and the same graceful-fallback-never-throws discipline, so
|
|
100
|
+
* this dispatcher inherits that guarantee: it always resolves to a draft
|
|
101
|
+
* (enriched or original), never rejects.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} draft - ConfigDraft from detectConfig()
|
|
104
|
+
* @param {object} [opts]
|
|
105
|
+
* @param {"cli"|"api"|"none"} [opts.backend] - Pre-resolved backend, to
|
|
106
|
+
* avoid a redundant CLI probe when the caller already called
|
|
107
|
+
* resolveEnrichBackend() for a UI gate/message decision. If omitted,
|
|
108
|
+
* resolved internally.
|
|
109
|
+
* @param {string} [opts.cwd] - Working directory (forwarded to whichever
|
|
110
|
+
* backend is selected; only meaningful for the cli backend).
|
|
111
|
+
* @param {object} [opts.env] - Environment. Used to resolve the backend when
|
|
112
|
+
* opts.backend is omitted, AND forwarded to the api backend
|
|
113
|
+
* (enrichViaApi) so it can read ANTHROPIC_API_KEY from the injected value
|
|
114
|
+
* instead of the real process.env — the test seam bin/journey.mjs relies
|
|
115
|
+
* on when passing ctx.env.
|
|
116
|
+
* @param {Function} [opts.isCliAvailableFn] - See resolveEnrichBackend().
|
|
117
|
+
* @returns {Promise<object>} Enriched ConfigDraft, or the original draft
|
|
118
|
+
* when no backend is available or the selected backend fails.
|
|
119
|
+
*/
|
|
120
|
+
export async function enrich(draft, opts = {}) {
|
|
121
|
+
const { cwd = process.cwd(), env = process.env, isCliAvailableFn, bin, spawnFn, timeoutMs } = opts;
|
|
122
|
+
const backend =
|
|
123
|
+
opts.backend ?? resolveEnrichBackend({ cwd, env, isCliAvailableFn });
|
|
124
|
+
|
|
125
|
+
if (backend === "cli") {
|
|
126
|
+
// bin/spawnFn/timeoutMs are test seams — forwarded when present so
|
|
127
|
+
// callers (and this module's own tests) can exercise the cli backend
|
|
128
|
+
// deterministically without depending on a real `claude` install.
|
|
129
|
+
const cliOpts = { cwd };
|
|
130
|
+
if (bin !== undefined) cliOpts.bin = bin;
|
|
131
|
+
if (spawnFn !== undefined) cliOpts.spawnFn = spawnFn;
|
|
132
|
+
if (timeoutMs !== undefined) cliOpts.timeoutMs = timeoutMs;
|
|
133
|
+
return enrichViaCli(draft, cliOpts);
|
|
134
|
+
}
|
|
135
|
+
if (backend === "api") return enrichViaApi(draft, { env });
|
|
136
|
+
return draft;
|
|
137
|
+
}
|