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
@@ -63,6 +63,16 @@ let markNudgeSeen;
63
63
  let parseForgeYaml;
64
64
  let sanitizeContextValue;
65
65
 
66
+ // ---------------------------------------------------------------------------
67
+ // init-detect helpers — populated inside the try block to honour fail-open
68
+ // ---------------------------------------------------------------------------
69
+
70
+ // Declared at module scope so it is accessible from handler functions
71
+ // (handleUnmanaged) called from within the try block. Assigned inside the
72
+ // try/catch so that any import failure (missing file, permissions error,
73
+ // syntax error) is caught and the hook still exits 0. <!-- fix: forge#1927 -->
74
+ let resolveGitRoot;
75
+
66
76
  // ---------------------------------------------------------------------------
67
77
  // Main — wrapped in try/catch to guarantee fail-open
68
78
  // ---------------------------------------------------------------------------
@@ -98,6 +108,21 @@ try {
98
108
  )
99
109
  );
100
110
 
111
+ // Import the git-root resolver from the same installation.
112
+ // Placed inside try/catch to honour the fail-open contract: if init-detect.mjs
113
+ // cannot be loaded (broken install, missing file, permissions error) the
114
+ // catch block below ensures we still exit 0 without blocking Claude Code.
115
+ //
116
+ // Use pathToFileURL() to convert the OS-native path to a file:// URL before
117
+ // passing it to dynamic import(). On Windows, join() produces backslash paths
118
+ // that import() rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME.
119
+ /** @type {import('../init-detect.mjs')} */
120
+ (
121
+ { resolveGitRoot } = await import(
122
+ pathToFileURL(join(FORGE_HOME, "bin", "init-detect.mjs")).href
123
+ )
124
+ );
125
+
101
126
  const cwd = process.cwd();
102
127
  const state = resolveState(cwd);
103
128
 
@@ -165,19 +190,32 @@ async function handleManagedActive(dir) {
165
190
  * Uses the registry to ensure the nudge fires at most once per directory.
166
191
  * After showing, records the directory so subsequent sessions are silent.
167
192
  *
193
+ * The nudge-tracking key is the *resolved git root*, not the raw `dir` — a
194
+ * user who launches Claude Code from a parent directory that merely contains
195
+ * the real git repo as its single subdirectory (e.g. `Projects/` containing
196
+ * `Projects/my-app/.git`) would otherwise have every sibling project under
197
+ * that same parent share one nudge-seen entry: the first project to trigger
198
+ * the nudge silently suppresses it for all the others. `resolveGitRoot`
199
+ * degrades gracefully (falls back to the unresolved `dir`) when the case
200
+ * doesn't apply, so this is safe to call unconditionally. <!-- fix: forge#1927 -->
201
+ *
168
202
  * @param {string} dir - Absolute path to the project directory.
169
203
  */
170
204
  async function handleUnmanaged(dir) {
171
- if (nudgeSeen(dir)) {
205
+ const trackingKey = resolveGitRoot(dir).root;
206
+
207
+ if (nudgeSeen(trackingKey)) {
172
208
  // Nudge already shown — stay silent
173
209
  return;
174
210
  }
175
211
 
176
- // Show the nudge, then record it so it won't show again
212
+ // Show the nudge, then record it so it won't show again. The displayed
213
+ // message still shows the raw `dir` the user is actually in — only the
214
+ // tracking key changes.
177
215
  console.log(buildNudgeContext(dir));
178
216
 
179
217
  // Best-effort — if this fails the nudge may show once more next session
180
- await markNudgeSeen(dir).catch(() => {});
218
+ await markNudgeSeen(trackingKey).catch(() => {});
181
219
  }
182
220
 
183
221
  // ---------------------------------------------------------------------------
@@ -223,12 +261,39 @@ function buildActiveContext(dir, forgeYaml) {
223
261
  200,
224
262
  );
225
263
 
264
+ // Default agent model — resolved from forge.yaml's optional
265
+ // `agents.default_model` field, falling back to "sonnet" (the hardcoded
266
+ // default every command spec's "Agent model policy" line already uses).
267
+ // Rendered unconditionally, like stagingBranch/featurePattern above, so
268
+ // command specs always have a concrete value to reference — not a
269
+ // conditional *Note line like milestoneNote, which is omitted when unset.
270
+ const defaultModel = sanitizeContextValue(
271
+ forgeYaml.agents?.default_model ?? "sonnet",
272
+ 100,
273
+ ) ?? "sonnet";
274
+
275
+ // Sub-agent model — resolved from forge.yaml's optional
276
+ // `agents.subagent_model` field. Governs child agents that a top-level
277
+ // command spawns internally (e.g. /orchestrate's per-issue /work-on
278
+ // agents, /review-pr's domain persona reviewers), which is a distinct
279
+ // hardcoded call-site from the top-level "Agent model policy" banner that
280
+ // `defaultModel` above already covers. Falls back to `defaultModel` (not
281
+ // directly to "sonnet") so a project that only sets `default_model` gets
282
+ // consistent behavior across both the main agent and its children.
283
+ // Rendered unconditionally, same as defaultModel above. <!-- issue #1852 -->
284
+ const subagentModel = sanitizeContextValue(
285
+ forgeYaml.agents?.subagent_model ?? defaultModel,
286
+ 100,
287
+ ) ?? defaultModel;
288
+
226
289
  return `\
227
290
  <!-- ForgeDock: managed-active -->
228
291
  **ForgeDock** is active in this directory (${safeDir}).
229
292
  ${nameNote}${repoNote}${descNote}${milestoneNote}
230
293
  - **Staging branch**: \`${stagingBranch}\`
231
294
  - **Feature branch pattern**: \`${featurePattern}\`
295
+ - **Default agent model**: \`${defaultModel}\`
296
+ - **Sub-agent model**: \`${subagentModel}\`
232
297
 
233
298
  ### Available pipeline commands
234
299
 
@@ -2,7 +2,11 @@
2
2
  /**
3
3
  * init-detect.mjs — Pure deterministic config detection for ForgeDock.
4
4
  *
5
- * Exports a single function: detectConfig(cwd)
5
+ * Exports:
6
+ * detectConfig(cwd) → Promise<ConfigDraft> (main deterministic detector)
7
+ * resolveGitRoot(cwd) → { root, resolved, why } (git-root resolution,
8
+ * reused outside this module by session-start.mjs's
9
+ * nudge-tracking path — see forge#1927)
6
10
  *
7
11
  * Returns a ConfigDraft: a structured object mirroring forge.yaml's required
8
12
  * sections (project, paths, branches) where every leaf is:
@@ -21,7 +25,8 @@
21
25
  */
22
26
 
23
27
  import { execFileSync } from "child_process";
24
- import { join } from "path";
28
+ import { join, resolve } from "path";
29
+ import { readdirSync, existsSync } from "fs";
25
30
 
26
31
  // ---------------------------------------------------------------------------
27
32
  // Field factory helpers
@@ -228,13 +233,88 @@ function detectStagingBranch(cwd, defaultBranch) {
228
233
  // git not available or no remotes
229
234
  }
230
235
 
231
- return medium(
236
+ // No 'origin/staging' found. Deriving a merge target from a failed lookup
237
+ // is not a "likely correct" inference — it's a guess, and `staging` is the
238
+ // single most destructive field to get wrong (it's the fast-lane PR merge
239
+ // target). Flag it "low" so it gets a # TODO comment and the [low] badge
240
+ // instead of silently blending in as if it were verified. <!-- Added: forge#1850 -->
241
+ return low(
232
242
  defaultBranch,
233
- "derived from default branch",
234
- `No 'origin/staging' remote branch found; staging defaults to the default branch '${defaultBranch}'`,
243
+ "default placeholder",
244
+ `No 'origin/staging' remote branch found; guessed the default branch '${defaultBranch}' — verify this is actually your staging branch`,
235
245
  );
236
246
  }
237
247
 
248
+ /**
249
+ * Resolve the effective git repository root for detection purposes.
250
+ *
251
+ * `execFileSync("git", ...)` already searches UPWARD through parent
252
+ * directories to find a repo root, so a `cwd` nested inside a repo is
253
+ * already handled correctly with no extra code here. The gap this closes is
254
+ * the other direction: `cwd` is a *parent* of the actual repo (a common
255
+ * monorepo-adjacent layout, e.g. `ScraperAPI/` containing the real repo at
256
+ * `ScraperAPI/alterlab/`) — git has no way to discover a repo by looking
257
+ * downward, so every detector below would otherwise degrade straight to
258
+ * placeholders.
259
+ *
260
+ * Never throws; falls back to the original `cwd` unchanged when neither
261
+ * case applies (no repo found at all, or the subdirectory scan is
262
+ * ambiguous — zero or multiple candidates).
263
+ *
264
+ * Exported for reuse outside `detectConfig()` — `bin/hooks/session-start.mjs`
265
+ * uses it to resolve the nudge-tracking key so that a directory which is a
266
+ * parent of the real git repo doesn't collide with sibling projects sharing
267
+ * the same parent. <!-- Added: forge#1927 -->
268
+ *
269
+ * @param {string} cwd
270
+ * @returns {{ root: string, resolved: boolean, why: string }}
271
+ */
272
+ export function resolveGitRoot(cwd) {
273
+ // cwd is already inside a repo (possibly nested) — `--show-toplevel` finds
274
+ // the true root regardless of depth. Compare with path.resolve() so a pure
275
+ // separator-style difference (git always emits forward slashes, even on
276
+ // Windows) doesn't get mistaken for an actual resolution.
277
+ try {
278
+ const toplevel = execFileSync(
279
+ "git",
280
+ ["rev-parse", "--show-toplevel"],
281
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 },
282
+ ).trim();
283
+ if (toplevel) {
284
+ const resolved = resolve(toplevel) !== resolve(cwd);
285
+ return {
286
+ root: resolved ? toplevel : cwd,
287
+ resolved,
288
+ why: `'${cwd}' is nested inside a git repository rooted at '${toplevel}'`,
289
+ };
290
+ }
291
+ } catch {
292
+ // Not inside any git repository — try the subdirectory scan below.
293
+ }
294
+
295
+ // cwd itself isn't a repo — check whether it holds exactly one immediate
296
+ // subdirectory that is. Ambiguous (0 or 2+) candidates fall through
297
+ // unchanged, preserving the existing placeholder behavior.
298
+ try {
299
+ const entries = readdirSync(cwd, { withFileTypes: true });
300
+ const gitDirs = entries.filter(
301
+ (e) => e.isDirectory() && existsSync(join(cwd, e.name, ".git")),
302
+ );
303
+ if (gitDirs.length === 1) {
304
+ const sub = join(cwd, gitDirs[0].name);
305
+ return {
306
+ root: sub,
307
+ resolved: true,
308
+ why: `'${cwd}' is not a git repository, but its only subdirectory '${gitDirs[0].name}' is — resolved detection to that subdirectory`,
309
+ };
310
+ }
311
+ } catch {
312
+ // cwd unreadable — fall through to the unresolved case.
313
+ }
314
+
315
+ return { root: cwd, resolved: false, why: "" };
316
+ }
317
+
238
318
  /**
239
319
  * Derive a human-readable project name from the repository slug.
240
320
  *
@@ -295,23 +375,32 @@ function deriveProjectName(repoSlug) {
295
375
  * @returns {Promise<ConfigDraft>}
296
376
  */
297
377
  export async function detectConfig(cwd = process.cwd()) {
378
+ // Resolve the effective repo root FIRST — every detector below runs
379
+ // against this, not the raw cwd argument. See resolveGitRoot() docblock.
380
+ const { root: gitRoot, resolved: rootResolved, why: rootWhy } = resolveGitRoot(cwd);
381
+
298
382
  // Project identity
299
- const { owner, repo, remoteDetected } = detectRemote(cwd);
383
+ const { owner, repo, remoteDetected } = detectRemote(gitRoot);
300
384
 
301
385
  // Project name derived from the repo slug
302
386
  const name = deriveProjectName(repo.value);
303
387
 
304
- // Paths — always high confidence (derived from the cwd argument)
305
- const root = high(cwd, "process.cwd()", "Absolute path passed to detectConfig — the project root");
388
+ // Paths — high confidence in the common case (root === cwd, nothing to
389
+ // resolve). When detection had to redirect to a git subdirectory or a
390
+ // parent toplevel, flag it medium confidence with an explanatory `why` so
391
+ // the review screen surfaces the redirection instead of silently adopting it.
392
+ const root = rootResolved
393
+ ? medium(gitRoot, "resolved git repository root", rootWhy)
394
+ : high(cwd, "process.cwd()", "Absolute path passed to detectConfig — the project root");
306
395
  const worktreeBase = high(
307
- join(cwd, ".claude", "worktrees"),
396
+ join(gitRoot, ".claude", "worktrees"),
308
397
  "derived from root",
309
- `Convention: {root}/.claude/worktrees (root = ${cwd})`,
398
+ `Convention: {root}/.claude/worktrees (root = ${gitRoot})`,
310
399
  );
311
400
 
312
401
  // Branch detection
313
- const defaultBranchField = detectDefaultBranch(cwd);
314
- const stagingBranchField = detectStagingBranch(cwd, defaultBranchField.value);
402
+ const defaultBranchField = detectDefaultBranch(gitRoot);
403
+ const stagingBranchField = detectStagingBranch(gitRoot, defaultBranchField.value);
315
404
 
316
405
  return {
317
406
  project: { owner, repo, name },