forgedock 1.1.8 → 1.1.10

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 (84) hide show
  1. package/README.md +10 -33
  2. package/bin/cinema.mjs +23 -5
  3. package/bin/engine-cli.mjs +137 -11
  4. package/bin/env-detect.mjs +249 -0
  5. package/bin/forge-utils.mjs +364 -4
  6. package/bin/forgedock.mjs +1064 -135
  7. package/bin/hooks/session-start.mjs +68 -3
  8. package/bin/init-detect.mjs +101 -12
  9. package/bin/journey.mjs +935 -19
  10. package/bin/recall.mjs +24 -7
  11. package/bin/registry.mjs +91 -3
  12. package/bin/runner.mjs +38 -2
  13. package/bin/tui.mjs +49 -3
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +8 -8
  16. package/commands/audit.md +1 -1
  17. package/commands/autopilot.md +1 -1
  18. package/commands/ci-audit.md +1 -1
  19. package/commands/cleanup.md +87 -52
  20. package/commands/compat-audit.md +1 -1
  21. package/commands/deploy-info.md +1 -1
  22. package/commands/deploy-pr.md +1 -1
  23. package/commands/diagnose.md +1 -1
  24. package/commands/explain.md +1 -1
  25. package/commands/fix-ci.md +1 -1
  26. package/commands/forgedock-init.md +1 -1
  27. package/commands/incident-response.md +3 -3
  28. package/commands/issue.md +1 -1
  29. package/commands/optimize.md +1 -1
  30. package/commands/orchestrate/config.md +13 -3
  31. package/commands/orchestrate/phase-1-resolve.md +1 -1
  32. package/commands/orchestrate/phase-2-triage.md +1 -1
  33. package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
  34. package/commands/orchestrate/phase-3-dependency.md +128 -20
  35. package/commands/orchestrate/phase-4-execution.md +598 -43
  36. package/commands/orchestrate/phase-5-cleanup.md +1 -1
  37. package/commands/orchestrate/phase-6-report.md +51 -3
  38. package/commands/orchestrate/safety.md +2 -2
  39. package/commands/qa-sweep.md +1 -1
  40. package/commands/quality-gate.md +1 -1
  41. package/commands/recover-orphans.md +5 -5
  42. package/commands/replay.md +1 -1
  43. package/commands/review-pr-agents/api.md +2 -2
  44. package/commands/review-pr-agents/auth.md +2 -2
  45. package/commands/review-pr-agents/billing.md +2 -2
  46. package/commands/review-pr-agents/concurrency.md +2 -2
  47. package/commands/review-pr-agents/database.md +2 -2
  48. package/commands/review-pr-agents/frontend.md +2 -2
  49. package/commands/review-pr-agents/infra.md +2 -2
  50. package/commands/review-pr-agents/protocols.md +1 -1
  51. package/commands/review-pr-agents/scraper.md +2 -2
  52. package/commands/review-pr-agents/security.md +2 -2
  53. package/commands/review-pr-staging.md +41 -3
  54. package/commands/review-pr.md +71 -6
  55. package/commands/rollback.md +1 -1
  56. package/commands/scope.md +1 -1
  57. package/commands/security-audit.md +1 -1
  58. package/commands/test-gate.md +1 -1
  59. package/commands/validate.md +1 -1
  60. package/commands/work-on/build/architect.md +1 -1
  61. package/commands/work-on/build/context.md +9 -6
  62. package/commands/work-on/build/implement.md +1 -1
  63. package/commands/work-on/build/validate.md +1 -1
  64. package/commands/work-on/build.md +31 -6
  65. package/commands/work-on/close.md +3 -2
  66. package/commands/work-on/decompose.md +1 -1
  67. package/commands/work-on/investigate.md +14 -5
  68. package/commands/work-on/remediate.md +331 -0
  69. package/commands/work-on/review.md +1 -1
  70. package/commands/work-on-monolithic.md +1 -1
  71. package/commands/work-on.md +194 -56
  72. package/docs/CI.md +2 -2
  73. package/docs/CODEX.md +1 -1
  74. package/docs/CONFIG.md +186 -0
  75. package/docs/site/command-reference.md +31 -1
  76. package/docs/site/getting-started.md +9 -34
  77. package/docs/site/index.md +1 -1
  78. package/docs/spec/label-state-machine.md +1 -1
  79. package/package.json +2 -1
  80. package/packages/protocol/src/types.js +1 -1
  81. package/scripts/check-command-docs-drift.sh +4 -2
  82. package/scripts/danger-zones.mjs +19 -12
  83. package/scripts/transition-label.sh +101 -13
  84. package/scripts/refresh-bot-token.sh +0 -97
@@ -6,13 +6,22 @@
6
6
  * top-level as part of its fail-open contract).
7
7
  *
8
8
  * Exports:
9
- * parseForgeYaml(raw) → object (parse a forge.yaml string)
10
- * sanitizeContextValue(value, max) → string|null (sanitize before injection)
9
+ * parseForgeYaml(raw) → object (parse a forge.yaml string)
10
+ * sanitizeContextValue(value, max) → string|null (sanitize before injection)
11
+ * resolveModelAlias(value) → string|null (short alias → full model ID)
12
+ * parseNameStatusDiff(diffText) → array (parse `git diff --name-status` output)
13
+ * classifyCommandChanges(entries, opts) → object (added/updated/removed/engine counts)
14
+ * countBreakingCommits(subjects) → number (breaking-change commit count)
15
+ * parseGitHubOwnerRepo(remoteUrl) → object|null ({owner, repo} from a git remote URL)
16
+ * classifyConventionalCommitLines(body) → object (feat/fix/... counts from release notes)
17
+ * formatUpdateChangelogSummary(opts) → string (git-clone mode changelog summary)
18
+ * formatVersionAvailableSummary(opts) → string (npm mode changelog summary)
11
19
  *
12
20
  * Contract guarantees:
13
21
  * - Pure: no I/O, no side effects, no global state
14
- * - Safe: both functions are try/catch-free at the module level; individual
15
- * helpers never throw
22
+ * - Safe: every exported function is internally try/catch-free where it
23
+ * cannot throw on any input shape (string coercion + defensive defaults);
24
+ * callers doing I/O (execFileSync, fetch) still wrap those calls
16
25
  * - Isolated: imports no external modules
17
26
  */
18
27
 
@@ -162,3 +171,354 @@ export function sanitizeContextValue(value, maxLen) {
162
171
  return null;
163
172
  }
164
173
  }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Model alias resolution
177
+ // ---------------------------------------------------------------------------
178
+
179
+ /**
180
+ * Short model aliases mapped to their current full Anthropic model IDs.
181
+ *
182
+ * These are the same aliases accepted by the Agent/Task tool's `model`
183
+ * parameter in Claude Code (enum: "sonnet" | "opus" | "haiku" | "fable").
184
+ * forge.yaml's `agents.default_model` field is documented to use one of
185
+ * these aliases so the same configured value drives both interactive
186
+ * (Agent/Task tool) and headless (bin/runner.mjs, raw Anthropic SDK) use.
187
+ *
188
+ * Keep in sync with the other hardcoded model IDs in this codebase (e.g.
189
+ * bin/runner.mjs's DEFAULT_MODEL, bin/init-enrich-api.mjs's ENRICH_MODEL)
190
+ * if those are ever bumped.
191
+ */
192
+ const MODEL_ALIASES = {
193
+ sonnet: "claude-sonnet-5",
194
+ opus: "claude-opus-4-6",
195
+ haiku: "claude-haiku-4-5",
196
+ };
197
+
198
+ /**
199
+ * Resolve a forge.yaml `agents.default_model` value to a full Anthropic
200
+ * model ID, for use with the raw Anthropic SDK (bin/runner.mjs).
201
+ *
202
+ * Recognized short aliases ("sonnet", "opus", "haiku" — case-insensitive)
203
+ * are mapped to their current full model ID via MODEL_ALIASES. Any other
204
+ * non-empty string is returned unchanged (pass-through), so a full model ID
205
+ * configured directly still works for the headless runner — though it will
206
+ * NOT work for interactive Agent/Task tool calls in command specs, which
207
+ * only accept the short-alias enum. This constraint is documented in
208
+ * docs/CONFIG.md and forge.yaml.example.
209
+ *
210
+ * Never throws. Returns null for null/undefined/empty input.
211
+ *
212
+ * @param {string|null|undefined} value - Raw `agents.default_model` value.
213
+ * @returns {string|null}
214
+ */
215
+ export function resolveModelAlias(value) {
216
+ try {
217
+ if (value == null) return null;
218
+ const trimmed = String(value).trim();
219
+ if (!trimmed) return null;
220
+ const alias = MODEL_ALIASES[trimmed.toLowerCase()];
221
+ return alias ?? trimmed;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Diff-aware changelog summary (forge#1947) — parsing & formatting helpers
229
+ // ---------------------------------------------------------------------------
230
+ //
231
+ // These functions turn raw `git diff --name-status`, `git log --pretty=%s`,
232
+ // a git remote URL, and a GitHub release body into the condensed changelog
233
+ // summary printed by `update()` in bin/forgedock.mjs. Kept pure/I/O-free here
234
+ // so they're unit-testable without spawning git or hitting the network — the
235
+ // caller (forgedock.mjs) is responsible for running the actual git/fetch
236
+ // calls and wrapping them in try/catch so a failure never blocks `update()`.
237
+
238
+ /**
239
+ * Parse the output of `git diff --name-status <a> <b>` into structured
240
+ * entries.
241
+ *
242
+ * Handles:
243
+ * - Plain status lines: `A\tpath`, `M\tpath`, `D\tpath`
244
+ * - Rename/copy lines: `R100\told\tnew` / `C100\told\tnew` — the new path
245
+ * is used, status is normalized to the bare letter (`R`/`C`)
246
+ * - Both LF and CRLF line endings
247
+ * - Blank lines and malformed lines are skipped rather than throwing
248
+ *
249
+ * @param {string} diffText - Raw stdout from `git diff --name-status`.
250
+ * @returns {Array<{status: string, path: string}>}
251
+ */
252
+ export function parseNameStatusDiff(diffText) {
253
+ const entries = [];
254
+ if (typeof diffText !== "string" || !diffText.trim()) return entries;
255
+
256
+ for (const line of diffText.split(/\r?\n/)) {
257
+ if (!line.trim()) continue;
258
+ const parts = line.split("\t").filter((p) => p.length > 0);
259
+ if (parts.length < 2) continue;
260
+
261
+ const rawStatus = parts[0];
262
+ const status = rawStatus.charAt(0).toUpperCase();
263
+ // Rename/copy lines carry two paths (old, new) — the working path after
264
+ // the change is the last field.
265
+ const path = parts[parts.length - 1];
266
+ if (!path) continue;
267
+
268
+ entries.push({ status, path });
269
+ }
270
+
271
+ return entries;
272
+ }
273
+
274
+ /**
275
+ * Classify parsed diff entries into command/engine change counts.
276
+ *
277
+ * `commands/` files are ForgeDock's slash-command specs — Added counts as
278
+ * "commands added", Deleted as "removed", everything else (Modified,
279
+ * Renamed, Copied) as "updated". `bin/engine/` files are counted as a single
280
+ * "engine changed" bucket regardless of status, since the changelog summary
281
+ * only needs to flag that the durable-run engine moved, not the granularity
282
+ * of how.
283
+ *
284
+ * @param {Array<{status: string, path: string}>} entries - From parseNameStatusDiff.
285
+ * @param {{commandsPrefix?: string, enginePrefix?: string}} [opts]
286
+ * @returns {{commandsAdded: number, commandsUpdated: number, commandsRemoved: number, engineChanged: number}}
287
+ */
288
+ export function classifyCommandChanges(entries, opts = {}) {
289
+ const commandsPrefix = opts.commandsPrefix ?? "commands/";
290
+ const enginePrefix = opts.enginePrefix ?? "bin/engine/";
291
+
292
+ const result = {
293
+ commandsAdded: 0,
294
+ commandsUpdated: 0,
295
+ commandsRemoved: 0,
296
+ engineChanged: 0,
297
+ };
298
+
299
+ if (!Array.isArray(entries)) return result;
300
+
301
+ for (const entry of entries) {
302
+ if (!entry || typeof entry.path !== "string") continue;
303
+ const normalizedPath = entry.path.replace(/\\/g, "/");
304
+
305
+ if (normalizedPath.startsWith(enginePrefix)) {
306
+ result.engineChanged++;
307
+ continue;
308
+ }
309
+
310
+ if (normalizedPath.startsWith(commandsPrefix)) {
311
+ if (entry.status === "A") result.commandsAdded++;
312
+ else if (entry.status === "D") result.commandsRemoved++;
313
+ else result.commandsUpdated++;
314
+ }
315
+ }
316
+
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Count commit subject lines that signal a breaking change, using the
322
+ * Conventional Commits convention: a `!` immediately before the `:` in the
323
+ * type/scope prefix (e.g. `feat(cli)!: drop legacy flag`), or a
324
+ * `BREAKING CHANGE` marker anywhere in the subject.
325
+ *
326
+ * @param {string[]} subjects - Commit subject lines (e.g. from `git log --pretty=%s`).
327
+ * @returns {number}
328
+ */
329
+ export function countBreakingCommits(subjects) {
330
+ if (!Array.isArray(subjects)) return 0;
331
+ const breakingPrefix = /^\w+(\([^)]*\))?!:/;
332
+ let count = 0;
333
+ for (const subject of subjects) {
334
+ if (typeof subject !== "string") continue;
335
+ if (breakingPrefix.test(subject) || subject.includes("BREAKING CHANGE")) {
336
+ count++;
337
+ }
338
+ }
339
+ return count;
340
+ }
341
+
342
+ /**
343
+ * Extract `{owner, repo}` from a git remote URL, supporting the common forms
344
+ * GitHub remotes take:
345
+ * - `https://github.com/owner/repo.git`
346
+ * - `https://github.com/owner/repo`
347
+ * - `git@github.com:owner/repo.git`
348
+ * - `ssh://git@github.com/owner/repo.git`
349
+ *
350
+ * Returns null (never throws) for non-GitHub remotes or unparseable input —
351
+ * callers should treat null as "omit the link" rather than failing.
352
+ *
353
+ * @param {string} remoteUrl - Output of `git remote get-url origin`.
354
+ * @returns {{owner: string, repo: string}|null}
355
+ */
356
+ export function parseGitHubOwnerRepo(remoteUrl) {
357
+ if (typeof remoteUrl !== "string" || !remoteUrl.trim()) return null;
358
+ const trimmed = remoteUrl.trim();
359
+
360
+ const patterns = [
361
+ /^(?:https?:\/\/)(?:[^@/]+@)?github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
362
+ /^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
363
+ /^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/,
364
+ ];
365
+
366
+ for (const pattern of patterns) {
367
+ const match = trimmed.match(pattern);
368
+ if (match) {
369
+ return { owner: match[1], repo: match[2] };
370
+ }
371
+ }
372
+
373
+ return null;
374
+ }
375
+
376
+ /**
377
+ * Classify a GitHub auto-generated release body ("What's Changed" style —
378
+ * a bullet list of `* type(scope): description by @user in .../pull/N`
379
+ * lines) into conventional-commit-type counts, plus a breaking-change count.
380
+ *
381
+ * Unrecognized/non-conventional bullet lines are counted under "other" so
382
+ * the total always reflects every listed entry.
383
+ *
384
+ * @param {string} body - Raw release body (markdown).
385
+ * @returns {{counts: Record<string, number>, breakingCount: number}}
386
+ */
387
+ export function classifyConventionalCommitLines(body) {
388
+ const counts = {};
389
+ let breakingCount = 0;
390
+ if (typeof body !== "string" || !body.trim()) {
391
+ return { counts, breakingCount };
392
+ }
393
+
394
+ const bulletPattern = /^\s*[*-]\s+(.+)$/;
395
+ const typePattern = /^(\w+)(\([^)]*\))?(!)?:\s*/;
396
+
397
+ for (const line of body.split(/\r?\n/)) {
398
+ const bulletMatch = line.match(bulletPattern);
399
+ if (!bulletMatch) continue;
400
+ const rest = bulletMatch[1];
401
+
402
+ const typeMatch = rest.match(typePattern);
403
+ const type = typeMatch ? typeMatch[1].toLowerCase() : "other";
404
+ counts[type] = (counts[type] || 0) + 1;
405
+
406
+ if ((typeMatch && typeMatch[3] === "!") || rest.includes("BREAKING CHANGE")) {
407
+ breakingCount++;
408
+ }
409
+ }
410
+
411
+ return { counts, breakingCount };
412
+ }
413
+
414
+ /**
415
+ * Build the condensed changelog summary printed after a successful
416
+ * git-clone-mode update (`update()`'s fast-forward-merge branch in
417
+ * bin/forgedock.mjs). Always returns at least a headline — never an empty
418
+ * string — so the caller can print unconditionally when it has data to pass.
419
+ *
420
+ * @param {object} opts
421
+ * @param {string} [opts.fromVersion] - package.json version before the merge.
422
+ * @param {string} [opts.toVersion] - package.json version after the merge.
423
+ * @param {number} [opts.commandsAdded]
424
+ * @param {number} [opts.commandsUpdated]
425
+ * @param {number} [opts.commandsRemoved]
426
+ * @param {number} [opts.engineChanged]
427
+ * @param {number} [opts.breakingCount]
428
+ * @param {string} [opts.compareUrl] - Link to the full diff/changelog.
429
+ * @returns {string}
430
+ */
431
+ export function formatUpdateChangelogSummary(opts = {}) {
432
+ const {
433
+ fromVersion,
434
+ toVersion,
435
+ commandsAdded = 0,
436
+ commandsUpdated = 0,
437
+ commandsRemoved = 0,
438
+ engineChanged = 0,
439
+ breakingCount = 0,
440
+ compareUrl,
441
+ } = opts;
442
+
443
+ const versionLabel =
444
+ fromVersion && toVersion && fromVersion !== toVersion
445
+ ? `v${fromVersion} -> v${toVersion}`
446
+ : toVersion
447
+ ? `v${toVersion}`
448
+ : null;
449
+
450
+ const bullets = [];
451
+ if (commandsAdded) {
452
+ bullets.push(`${commandsAdded} command${commandsAdded === 1 ? "" : "s"} added`);
453
+ }
454
+ if (commandsRemoved) {
455
+ bullets.push(`${commandsRemoved} removed`);
456
+ }
457
+ if (commandsUpdated) {
458
+ bullets.push(`${commandsUpdated} updated`);
459
+ }
460
+ if (engineChanged) {
461
+ bullets.push(`engine: ${engineChanged} file${engineChanged === 1 ? "" : "s"} changed`);
462
+ }
463
+
464
+ const headline = versionLabel ? `Updated ${versionLabel}` : "Updated";
465
+ const lines = [bullets.length ? `${headline}: ${bullets.join(", ")}` : `${headline}.`];
466
+
467
+ if (breakingCount > 0) {
468
+ lines.push(
469
+ ` ${breakingCount} breaking change${breakingCount === 1 ? "" : "s"} — review before continuing.`,
470
+ );
471
+ }
472
+ if (compareUrl) {
473
+ lines.push(` See full changelog: ${compareUrl}`);
474
+ }
475
+
476
+ return lines.join("\n");
477
+ }
478
+
479
+ /**
480
+ * Build the condensed changelog summary printed after `update()` detects a
481
+ * newer version is available in npm/npx mode (where the tool never runs the
482
+ * update itself, so no local before/after diff exists — the summary is
483
+ * sourced from the newest published GitHub release's notes instead).
484
+ *
485
+ * @param {object} opts
486
+ * @param {string} opts.currentVersion - Installed package.json version.
487
+ * @param {string} opts.latestVersion - Latest published version.
488
+ * @param {Record<string, number>} [opts.typeCounts] - From classifyConventionalCommitLines.
489
+ * @param {number} [opts.breakingCount]
490
+ * @param {string} [opts.releaseUrl] - Link to the release notes.
491
+ * @returns {string}
492
+ */
493
+ export function formatVersionAvailableSummary(opts = {}) {
494
+ const {
495
+ currentVersion,
496
+ latestVersion,
497
+ typeCounts = {},
498
+ breakingCount = 0,
499
+ releaseUrl,
500
+ } = opts;
501
+
502
+ const parts = Object.entries(typeCounts)
503
+ .filter(([, count]) => count > 0)
504
+ .sort((a, b) => b[1] - a[1])
505
+ .map(([type, count]) => `${count} ${type}`);
506
+
507
+ const headline =
508
+ currentVersion && latestVersion
509
+ ? `Changelog v${currentVersion} -> v${latestVersion}`
510
+ : "Changelog";
511
+
512
+ const lines = [parts.length ? `${headline}: ${parts.join(", ")}` : `${headline}.`];
513
+
514
+ if (breakingCount > 0) {
515
+ lines.push(
516
+ ` ${breakingCount} breaking change${breakingCount === 1 ? "" : "s"} — review before updating.`,
517
+ );
518
+ }
519
+ if (releaseUrl) {
520
+ lines.push(` See full changelog: ${releaseUrl}`);
521
+ }
522
+
523
+ return lines.join("\n");
524
+ }