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.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +935 -19
- package/bin/recall.mjs +24 -7
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +1 -1
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +87 -52
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +1 -1
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +128 -20
- package/commands/orchestrate/phase-4-execution.md +598 -43
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +51 -3
- package/commands/orchestrate/safety.md +2 -2
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +1 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +71 -6
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +1 -1
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/context.md +9 -6
- package/commands/work-on/build/implement.md +1 -1
- package/commands/work-on/build/validate.md +1 -1
- package/commands/work-on/build.md +31 -6
- package/commands/work-on/close.md +3 -2
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +14 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +194 -56
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/label-state-machine.md +1 -1
- package/package.json +2 -1
- package/packages/protocol/src/types.js +1 -1
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +19 -12
- package/scripts/transition-label.sh +101 -13
- 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
|
-
|
|
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(
|
|
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
|
|
package/bin/init-detect.mjs
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* init-detect.mjs — Pure deterministic config detection for ForgeDock.
|
|
4
4
|
*
|
|
5
|
-
* Exports
|
|
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
|
-
|
|
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
|
-
"
|
|
234
|
-
`No 'origin/staging' remote branch found;
|
|
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(
|
|
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 —
|
|
305
|
-
|
|
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(
|
|
396
|
+
join(gitRoot, ".claude", "worktrees"),
|
|
308
397
|
"derived from root",
|
|
309
|
-
`Convention: {root}/.claude/worktrees (root = ${
|
|
398
|
+
`Convention: {root}/.claude/worktrees (root = ${gitRoot})`,
|
|
310
399
|
);
|
|
311
400
|
|
|
312
401
|
// Branch detection
|
|
313
|
-
const defaultBranchField = detectDefaultBranch(
|
|
314
|
-
const stagingBranchField = detectStagingBranch(
|
|
402
|
+
const defaultBranchField = detectDefaultBranch(gitRoot);
|
|
403
|
+
const stagingBranchField = detectStagingBranch(gitRoot, defaultBranchField.value);
|
|
315
404
|
|
|
316
405
|
return {
|
|
317
406
|
project: { owner, repo, name },
|