forgedock 1.1.11 → 1.3.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 (109) hide show
  1. package/README.md +9 -3
  2. package/bin/cli-spawn-shared.mjs +25 -0
  3. package/bin/engine/phases.mjs +178 -28
  4. package/bin/engine/state.mjs +39 -7
  5. package/bin/engine-cli.mjs +85 -3
  6. package/bin/engine.mjs +71 -8
  7. package/bin/forgedock.mjs +322 -20
  8. package/bin/hooks/pre-tool-use.mjs +345 -10
  9. package/bin/init-enrich-api.mjs +12 -4
  10. package/bin/init-enrich-cli.mjs +328 -0
  11. package/bin/init-enrich.mjs +137 -0
  12. package/bin/journey.mjs +116 -15
  13. package/bin/runner.mjs +653 -10
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +10 -7
  16. package/commands/audit-agents.md +29 -6
  17. package/commands/audit.md +9 -20
  18. package/commands/autopilot.md +52 -78
  19. package/commands/changelog.md +1 -1
  20. package/commands/ci-audit.md +19 -10
  21. package/commands/cleanup.md +1 -1
  22. package/commands/compat-audit.md +1 -1
  23. package/commands/deploy-info.md +1 -1
  24. package/commands/deploy-pr.md +1 -1
  25. package/commands/design-bench.md +1 -1
  26. package/commands/design-render-critique-loop.md +1 -1
  27. package/commands/diagnose.md +1 -1
  28. package/commands/forge-stats.md +6 -5
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/geo-audit.md +9 -6
  31. package/commands/incident-response.md +15 -5
  32. package/commands/issue.md +302 -15
  33. package/commands/milestone.md +45 -18
  34. package/commands/optimize.md +1 -1
  35. package/commands/orchestrate/phase-1-resolve.md +20 -5
  36. package/commands/orchestrate/phase-4-execution.md +8 -1
  37. package/commands/orchestrate/phase-5-cleanup.md +43 -2
  38. package/commands/orchestrate/phase-6-report.md +1 -0
  39. package/commands/orchestrate.md +1 -1
  40. package/commands/pipeline-health/phase-4-proposals.md +40 -10
  41. package/commands/pipeline-health/phase-5-report.md +140 -13
  42. package/commands/pipeline-health.md +1 -1
  43. package/commands/pipeline-resume.md +1 -1
  44. package/commands/pipeline-status.md +1 -1
  45. package/commands/qa-sweep.md +27 -4
  46. package/commands/recover-orphans.md +1 -1
  47. package/commands/replay.md +16 -2
  48. package/commands/review-pr-agents/protocols.md +4 -0
  49. package/commands/review-pr-agents/spec-cli.md +179 -0
  50. package/commands/review-pr-agents.md +7 -4
  51. package/commands/review-pr-staging.md +44 -15
  52. package/commands/review-pr.md +122 -28
  53. package/commands/rollback.md +15 -5
  54. package/commands/security-audit.md +14 -11
  55. package/commands/signal-planner.md +32 -12
  56. package/commands/spec-doctor.md +1 -1
  57. package/commands/sync-ecosystem.md +54 -11
  58. package/commands/test-gate.md +49 -11
  59. package/commands/upgrade-deps.md +518 -0
  60. package/commands/validate.md +16 -5
  61. package/commands/work-on/build/architect.md +1 -1
  62. package/commands/work-on/build/context.md +1 -1
  63. package/commands/work-on/build/implement.md +2 -2
  64. package/commands/work-on/build/validate.md +64 -2
  65. package/commands/work-on/build.md +10 -2
  66. package/commands/work-on/close.md +1 -1
  67. package/commands/work-on/decompose.md +35 -25
  68. package/commands/work-on/investigate.md +2 -2
  69. package/commands/work-on/remediate.md +1 -1
  70. package/commands/work-on/review.md +5 -3
  71. package/commands/work-on-monolithic.md +1 -1
  72. package/commands/work-on.md +28 -7
  73. package/docs/AIDER.md +264 -0
  74. package/docs/AUTOPILOT-CRON.md +130 -0
  75. package/docs/CI.md +18 -0
  76. package/docs/CONFIG.md +41 -2
  77. package/docs/CURSOR.md +135 -0
  78. package/docs/OPENCODE.md +312 -0
  79. package/docs/demo.gif +0 -0
  80. package/docs/eval/README.md +4 -4
  81. package/docs/launch/show-hn.md +3 -3
  82. package/docs/site/annotations-explained.md +2 -2
  83. package/docs/site/architecture.md +180 -0
  84. package/docs/site/command-reference.md +38 -0
  85. package/docs/site/configuration.md +243 -0
  86. package/docs/site/for-companies.md +4 -4
  87. package/docs/site/forge-annotation-protocol.md +1 -1
  88. package/docs/site/github-is-the-memory.md +2 -2
  89. package/docs/site/how-it-works.md +1 -1
  90. package/docs/site/public/llms-full.txt +380 -0
  91. package/docs/site/public/llms.txt +35 -0
  92. package/docs/spec/review-protocol.md +6 -0
  93. package/docs/spec-graph-schema.md +3 -3
  94. package/package.json +2 -2
  95. package/packages/protocol/README.md +75 -0
  96. package/packages/protocol/src/card.js +114 -0
  97. package/packages/protocol/src/cli.js +4 -77
  98. package/packages/protocol/src/emit.js +14 -3
  99. package/packages/protocol/src/index.js +2 -1
  100. package/packages/protocol/src/parse.js +17 -8
  101. package/packages/protocol/src/types.js +1 -1
  102. package/packages/protocol/src/validate.js +11 -0
  103. package/scripts/check-command-side-effects.sh +261 -0
  104. package/scripts/check-spec-markers.sh +273 -0
  105. package/scripts/classify-lane.sh +29 -7
  106. package/scripts/forge-run.sh +322 -0
  107. package/templates/cross-repo-trigger.yml +282 -0
  108. package/templates/cursor/forge.mdc +148 -0
  109. package/templates/github-actions/forgedock-autopilot.yml +113 -0
@@ -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
+ }