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.
- package/README.md +9 -3
- package/bin/cli-spawn-shared.mjs +25 -0
- package/bin/engine/phases.mjs +178 -28
- package/bin/engine/state.mjs +39 -7
- package/bin/engine-cli.mjs +85 -3
- package/bin/engine.mjs +71 -8
- package/bin/forgedock.mjs +322 -20
- package/bin/hooks/pre-tool-use.mjs +345 -10
- 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 +116 -15
- 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 +6 -5
- 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 +302 -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 +8 -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 +27 -4
- package/commands/recover-orphans.md +1 -1
- package/commands/replay.md +16 -2
- package/commands/review-pr-agents/protocols.md +4 -0
- package/commands/review-pr-agents/spec-cli.md +179 -0
- package/commands/review-pr-agents.md +7 -4
- package/commands/review-pr-staging.md +44 -15
- package/commands/review-pr.md +122 -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/upgrade-deps.md +518 -0
- package/commands/validate.md +16 -5
- 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 +64 -2
- package/commands/work-on/build.md +10 -2
- package/commands/work-on/close.md +1 -1
- package/commands/work-on/decompose.md +35 -25
- package/commands/work-on/investigate.md +2 -2
- package/commands/work-on/remediate.md +1 -1
- package/commands/work-on/review.md +5 -3
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +28 -7
- package/docs/AIDER.md +264 -0
- package/docs/AUTOPILOT-CRON.md +130 -0
- package/docs/CI.md +18 -0
- package/docs/CONFIG.md +41 -2
- package/docs/CURSOR.md +135 -0
- package/docs/OPENCODE.md +312 -0
- package/docs/demo.gif +0 -0
- package/docs/eval/README.md +4 -4
- package/docs/launch/show-hn.md +3 -3
- package/docs/site/annotations-explained.md +2 -2
- package/docs/site/architecture.md +180 -0
- package/docs/site/command-reference.md +38 -0
- package/docs/site/configuration.md +243 -0
- package/docs/site/for-companies.md +4 -4
- package/docs/site/forge-annotation-protocol.md +1 -1
- package/docs/site/github-is-the-memory.md +2 -2
- package/docs/site/how-it-works.md +1 -1
- package/docs/site/public/llms-full.txt +380 -0
- package/docs/site/public/llms.txt +35 -0
- package/docs/spec/review-protocol.md +6 -0
- package/docs/spec-graph-schema.md +3 -3
- package/package.json +2 -2
- package/packages/protocol/README.md +75 -0
- package/packages/protocol/src/card.js +114 -0
- package/packages/protocol/src/cli.js +4 -77
- package/packages/protocol/src/emit.js +14 -3
- package/packages/protocol/src/index.js +2 -1
- package/packages/protocol/src/parse.js +17 -8
- package/packages/protocol/src/types.js +1 -1
- package/packages/protocol/src/validate.js +11 -0
- package/scripts/check-command-side-effects.sh +261 -0
- package/scripts/check-spec-markers.sh +273 -0
- package/scripts/classify-lane.sh +29 -7
- package/scripts/forge-run.sh +322 -0
- package/templates/cross-repo-trigger.yml +282 -0
- package/templates/cursor/forge.mdc +148 -0
- package/templates/github-actions/forgedock-autopilot.yml +113 -0
|
@@ -92,13 +92,43 @@ async function getInvariants() {
|
|
|
92
92
|
* setting env vars via Bash tool calls, as this hook reads process.env which
|
|
93
93
|
* is set at process start, not at tool-call time).
|
|
94
94
|
*
|
|
95
|
-
*
|
|
95
|
+
* 5. Filesystem-root `find` guard (issue #2034)
|
|
96
|
+
* Intercepts any Bash command containing a `find` invocation whose search
|
|
97
|
+
* root is `/` or a bare Git-Bash drive mount (`/c`, `/d`, ...) and
|
|
98
|
+
* hard-blocks it. On Windows Git Bash, `/` spans every mounted drive, so a
|
|
99
|
+
* root-anchored `find` never terminates and accumulates as an orphaned,
|
|
100
|
+
* CPU-exhausting process once its parent sub-agent exits (issue #1984 was
|
|
101
|
+
* a narrow, single-call-site precursor fix — this rule is the systemic,
|
|
102
|
+
* deterministic follow-up). No override — a root-anchored `find` has no
|
|
103
|
+
* legitimate use in any ForgeDock pipeline.
|
|
104
|
+
*
|
|
105
|
+
* 6. Attribution guard
|
|
106
|
+
* Intercepts commit / PR / issue / comment creation commands (`git commit`,
|
|
107
|
+
* `gh pr create|edit|comment`, `gh issue create|edit|comment`) and
|
|
108
|
+
* hard-blocks any whose message or body carries the Claude Code harness
|
|
109
|
+
* default attribution — "🤖 Generated with [Claude Code]", "Co-Authored-By:
|
|
110
|
+
* Claude", or the `noreply@anthropic.com` co-author trailer. Pipeline output
|
|
111
|
+
* is ForgeDock-branded; the assistant-tool attribution must never leak into
|
|
112
|
+
* a repo's public commit/PR/issue history. The agent is told to remove the
|
|
113
|
+
* attribution and, where a footer is wanted, use the ForgeDock signature.
|
|
114
|
+
* Override: set FORGE_ALLOW_AI_ATTRIBUTION=1 in the shell environment before
|
|
115
|
+
* starting Claude Code (operator-set only — same process.env semantics as
|
|
116
|
+
* the gist guard above).
|
|
117
|
+
*
|
|
118
|
+
* Rules 1-4 and 6 only apply inside a ForgeDock-managed directory (a directory
|
|
96
119
|
* with a `forge.yaml` or `.forgedock` marker) — see `isForgeDockManagedCwd()`
|
|
97
120
|
* (issue #1591). The hook installs into the user's global
|
|
98
121
|
* `~/.claude/settings.json`, so without this guard every Bash call in every
|
|
99
122
|
* repo on the machine would be subject to these ForgeDock-specific rules,
|
|
100
123
|
* including unrelated repos where `main` is a legitimate PR target.
|
|
101
124
|
*
|
|
125
|
+
* Rule 5 is deliberately NOT gated by `isForgeDockManagedCwd()` — it runs
|
|
126
|
+
* before that check. A filesystem-root `find` is a universal footgun with no
|
|
127
|
+
* legitimate use anywhere (unlike Rules 1-4/6, which are ForgeDock-pipeline-
|
|
128
|
+
* specific), and gating it the same way would leave it silently disabled
|
|
129
|
+
* inside git worktrees, which typically carry no `forge.yaml`/`.forgedock`
|
|
130
|
+
* marker of their own — exactly where build/review sub-agents run.
|
|
131
|
+
*
|
|
102
132
|
* === Fail-open contract ===
|
|
103
133
|
*
|
|
104
134
|
* Any uncaught error or parse failure exits 0 (allow) — this hook must
|
|
@@ -138,6 +168,94 @@ const FORBIDDEN_PR_BASES = ["main", "master"];
|
|
|
138
168
|
/** Labels that are always valid (non-workflow labels are not constrained). */
|
|
139
169
|
const WORKFLOW_LABEL_PREFIX = "workflow:";
|
|
140
170
|
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Rule 5 constants (issue #2034)
|
|
173
|
+
// Declared here — above the top-level `await main()` call — for the same
|
|
174
|
+
// reason the label-transition/PR-base constants above are: a top-level
|
|
175
|
+
// `await` suspends module evaluation at that statement, so any top-level
|
|
176
|
+
// `const` declared textually AFTER it (unlike hoisted `function` declarations)
|
|
177
|
+
// is still in its temporal dead zone when `main()` first runs, and would
|
|
178
|
+
// throw a ReferenceError the instant `checkFindRoot()` reads it — silently
|
|
179
|
+
// swallowed by main()'s fail-open catch, defeating the whole rule.
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Match a `find` search-root token that reaches the exact same whole-drive,
|
|
184
|
+
* non-terminating scan on Git Bash as the bare filesystem root (`/`) or a
|
|
185
|
+
* bare drive mount (`/c`, `/d`, ...). This includes trailing-slash drive
|
|
186
|
+
* mounts (`/c/`) — a natural way to write the same root, since `/c` and
|
|
187
|
+
* `/c/` are the identical directory — and the dot/double-slash root
|
|
188
|
+
* variants `/.`, `/..`, `//`, which likewise resolve to (or immediately
|
|
189
|
+
* above, which Git Bash still normalizes to) the same whole-drive tree
|
|
190
|
+
* (issue #2113 — PR #2112 review finding SEC-1: `find /c/` was silently
|
|
191
|
+
* ALLOWED because the prior regex had no trailing-slash/dot alternative).
|
|
192
|
+
*
|
|
193
|
+
* Deliberately does NOT match `/c/Users/...`, `/./foo`, `/../foo`, or any
|
|
194
|
+
* other scoped absolute path — only the bare root/drive-mount token itself,
|
|
195
|
+
* optionally followed by exactly one trailing slash (drive-letter case) or
|
|
196
|
+
* standing alone (dot/double-slash case). Case-insensitive on the drive
|
|
197
|
+
* letter only (POSIX paths are otherwise case-sensitive).
|
|
198
|
+
*/
|
|
199
|
+
const FIND_ROOT_TOKEN_RE = /^\/(?:[a-zA-Z]\/?|\.{1,2}|\/)?$/;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Shell metacharacters that can glue a command name to an adjacent token
|
|
203
|
+
* without whitespace (e.g. `cd /tmp;find /`, `$(find /...)`, `` `find /` ``,
|
|
204
|
+
* `echo x&&find /`). `tokenizeCommand()` only splits on whitespace and
|
|
205
|
+
* quotes, so without this extra split step a `find` immediately following
|
|
206
|
+
* one of these characters stays fused into the previous token (e.g. the
|
|
207
|
+
* single token `/tmp;find` or `$(find`) and is never recognized as a `find`
|
|
208
|
+
* invocation — a real, exploitable bypass of Rule 5 (issue #2034 review
|
|
209
|
+
* finding SEC-1). Declared here (above `try { await main(); }`) for the
|
|
210
|
+
* same temporal-dead-zone reason as `FIND_ROOT_TOKEN_RE` above.
|
|
211
|
+
*/
|
|
212
|
+
const SHELL_METACHAR_SPLIT_RE = /[;&|()$`]+/;
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Attribution guard constants (Rule 6)
|
|
216
|
+
// Declared here — above the top-level `await main()` call — so they are
|
|
217
|
+
// initialized before the hook runs, for the same temporal-dead-zone reason
|
|
218
|
+
// documented for the Rule 5 constants above. `checkAttribution` (a hoisted
|
|
219
|
+
// function declaration) is defined further down, but the const values it
|
|
220
|
+
// closes over must exist at call time or the reference throws into the
|
|
221
|
+
// fail-open catch, silently defeating the rule.
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Canonical ForgeDock signature line. The single source of truth for the
|
|
226
|
+
* pipeline's brand footer — command specs (work-on/review, review-pr,
|
|
227
|
+
* work-on/investigate) render this same wording. Kept here so the enforcement
|
|
228
|
+
* message can quote the exact replacement the agent should use.
|
|
229
|
+
*/
|
|
230
|
+
const FORGEDOCK_SIGNATURE =
|
|
231
|
+
"⚒️ Orchestrated with [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) — state, scheduling, review, and memory on GitHub.";
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Markers of the Claude Code harness default attribution. These are the exact
|
|
235
|
+
* signatures the assistant appends by default to commits and PR bodies; none
|
|
236
|
+
* of them belong in a repo's public commit/PR/issue history when the work is
|
|
237
|
+
* produced by the ForgeDock pipeline.
|
|
238
|
+
*
|
|
239
|
+
* Matching is case-insensitive and scoped (below) to commands that actually
|
|
240
|
+
* write a commit message or a GitHub body/comment, so mentions of "Claude
|
|
241
|
+
* Code" in unrelated commands (docs edits, greps) never trip this rule.
|
|
242
|
+
*/
|
|
243
|
+
const AI_ATTRIBUTION_MARKERS = [
|
|
244
|
+
/generated\s+with\s+\[?claude\s+code/i, // "🤖 Generated with [Claude Code](...)" / "Generated with Claude Code"
|
|
245
|
+
/co-authored-by:\s*claude/i, // "Co-Authored-By: Claude ..."
|
|
246
|
+
/noreply@anthropic\.com/i, // the anthropic co-author trailer email
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Commands that carry a commit message or a GitHub body/comment — the only
|
|
251
|
+
* surfaces where an attribution footer can be persisted to history.
|
|
252
|
+
*/
|
|
253
|
+
const ATTRIBUTION_SCOPED_COMMANDS = [
|
|
254
|
+
/git\s+commit\b/,
|
|
255
|
+
/gh\s+pr\s+(?:create|edit|comment)\b/,
|
|
256
|
+
/gh\s+issue\s+(?:create|edit|comment)\b/,
|
|
257
|
+
];
|
|
258
|
+
|
|
141
259
|
// ---------------------------------------------------------------------------
|
|
142
260
|
// Main — fail-open wrapper
|
|
143
261
|
// ---------------------------------------------------------------------------
|
|
@@ -168,13 +286,25 @@ async function main() {
|
|
|
168
286
|
// Only intercept Bash tool calls.
|
|
169
287
|
if (toolName !== "Bash") { process.exit(0); return; }
|
|
170
288
|
|
|
171
|
-
// Only enforce inside a ForgeDock-managed project — this hook installs
|
|
172
|
-
// globally (~/.claude/settings.json), so without this guard both rules
|
|
173
|
-
// below would fire in every unrelated repo on the machine (issue #1591).
|
|
174
|
-
if (!(await isForgeDockManagedCwd())) { process.exit(0); return; }
|
|
175
|
-
|
|
176
289
|
const command = String(toolInput.command || "");
|
|
177
290
|
|
|
291
|
+
// --- Rule 5: Filesystem-root `find` guard ---
|
|
292
|
+
// Deliberately checked BEFORE the isForgeDockManagedCwd() gate below — a
|
|
293
|
+
// root-anchored `find` is a universal footgun with no legitimate use in
|
|
294
|
+
// any directory (managed or not), and must fire inside git worktrees,
|
|
295
|
+
// which typically carry no forge.yaml/.forgedock marker (issue #2034).
|
|
296
|
+
const findRootViolation = checkFindRoot(command);
|
|
297
|
+
if (findRootViolation) {
|
|
298
|
+
process.stderr.write(findRootViolation);
|
|
299
|
+
process.exit(2);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Only enforce Rules 1-4 and 6 inside a ForgeDock-managed project — this hook
|
|
304
|
+
// installs globally (~/.claude/settings.json), so without this guard those
|
|
305
|
+
// rules would fire in every unrelated repo on the machine (issue #1591).
|
|
306
|
+
if (!(await isForgeDockManagedCwd())) { process.exit(0); return; }
|
|
307
|
+
|
|
178
308
|
// --- Rule 1: PR branch target validation ---
|
|
179
309
|
const prViolation = checkPrTarget(command);
|
|
180
310
|
if (prViolation) {
|
|
@@ -207,6 +337,14 @@ async function main() {
|
|
|
207
337
|
return;
|
|
208
338
|
}
|
|
209
339
|
|
|
340
|
+
// --- Rule 6: Attribution guard ---
|
|
341
|
+
const attributionViolation = checkAttribution(command);
|
|
342
|
+
if (attributionViolation) {
|
|
343
|
+
process.stderr.write(attributionViolation);
|
|
344
|
+
process.exit(2);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
210
348
|
process.exit(0);
|
|
211
349
|
}
|
|
212
350
|
|
|
@@ -508,6 +646,181 @@ function checkGistVisibility(command) {
|
|
|
508
646
|
return null;
|
|
509
647
|
}
|
|
510
648
|
|
|
649
|
+
// ---------------------------------------------------------------------------
|
|
650
|
+
// Rule 5: Filesystem-root `find` guard (issue #2034)
|
|
651
|
+
// ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Break each unquoted token from `tokenizeCommand()` on shell metacharacters
|
|
655
|
+
* (`;`, `&`, `|`, `(`, `)`, `$`, `` ` ``) to recover `find` as its own
|
|
656
|
+
* logical token even when it was written with no separating whitespace from
|
|
657
|
+
* a command separator or a command-substitution opener (`$(find ...)`).
|
|
658
|
+
*
|
|
659
|
+
* A token is only passed through UNSPLIT (treated as inert argument text —
|
|
660
|
+
* e.g. a `--body` value that merely *mentions* `find /` in prose) when its
|
|
661
|
+
* quoting actually spans EMBEDDED WHITESPACE, proving the quotes were used
|
|
662
|
+
* to glue multiple real words into one argument. `quoted` alone is NOT a
|
|
663
|
+
* sufficient signal: `tokenizeCommand()` sets `quoted = true` the moment ANY
|
|
664
|
+
* quote character appears anywhere in a token, including a degenerate empty
|
|
665
|
+
* pair (`""`, `''`) glued onto otherwise-unquoted text. A command like
|
|
666
|
+
* `cd /tmp;""find /` tokenizes to a single token `/tmp;find` flagged
|
|
667
|
+
* `quoted: true` even though nothing was actually protected by the empty
|
|
668
|
+
* quotes — real shells treat `""find` as the plain word `find`. Passing
|
|
669
|
+
* that token through unsplit hid `find` inside `/tmp;find`, which never
|
|
670
|
+
* exact-matches `"find"` in `checkFindRoot()`, bypassing the guard (issue
|
|
671
|
+
* #2059). This mirrors the exact discriminator `extractFlag()` already uses
|
|
672
|
+
* for the same class of decoy (issues #1519, #1591): "was quoted at all" is
|
|
673
|
+
* the wrong test; "does the token contain embedded whitespace" is right.
|
|
674
|
+
*
|
|
675
|
+
* @param {string} command
|
|
676
|
+
* @returns {string[]} Flattened list of logical token strings, in order.
|
|
677
|
+
*/
|
|
678
|
+
function extractLogicalTokens(command) {
|
|
679
|
+
const raw = tokenizeCommand(command);
|
|
680
|
+
const logical = [];
|
|
681
|
+
for (const { value, quoted } of raw) {
|
|
682
|
+
if (quoted && /\s/.test(value)) {
|
|
683
|
+
logical.push(value);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
for (const piece of value.split(SHELL_METACHAR_SPLIT_RE)) {
|
|
687
|
+
if (piece.length > 0) logical.push(piece);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return logical;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Check whether a Bash command contains a `find` invocation whose search
|
|
695
|
+
* root is the filesystem root (`/`), a bare Git-Bash drive mount (`/c`,
|
|
696
|
+
* `/d`, ...), a trailing-slash drive mount (`/c/`), or a dot/double-slash
|
|
697
|
+
* root variant (`/.`, `/..`, `//`). On Windows Git Bash, `/` spans every
|
|
698
|
+
* mounted drive, so a root-anchored `find` never terminates and accumulates
|
|
699
|
+
* as an orphaned, CPU-exhausting process (issue #2034 — #1984 was a narrow
|
|
700
|
+
* precursor fix that only patched a single call site + added a prose
|
|
701
|
+
* guardrail; this is the systemic, deterministic follow-up. Issue #2113
|
|
702
|
+
* widened the root-token match to also cover the trailing-slash/dot
|
|
703
|
+
* variants, which reach the identical whole-drive scan but were not
|
|
704
|
+
* originally matched by `FIND_ROOT_TOKEN_RE`).
|
|
705
|
+
*
|
|
706
|
+
* Scans ALL logical tokens for a `find` occurrence (not just the first word
|
|
707
|
+
* of the command), so it catches `find` appearing after `&&`, `;`, `|`, a
|
|
708
|
+
* command-substitution opener (`$(find ...)`), or backticks — not just a
|
|
709
|
+
* command that starts with `find`. Matching is case-insensitive on the
|
|
710
|
+
* command name (Windows resolves `Find`/`FIND` to the same binary as
|
|
711
|
+
* `find`) but NOT on the root-path token itself (POSIX paths are
|
|
712
|
+
* case-sensitive). Uses `extractLogicalTokens()` (quote-aware, and
|
|
713
|
+
* metacharacter-aware) so flag-shaped or path-shaped text embedded inside
|
|
714
|
+
* an unrelated quoted `--title`/`--body` value is never misread as a real
|
|
715
|
+
* `find` invocation (same class of decoy documented for `extractFlag()` —
|
|
716
|
+
* issues #1519, #1591), while a `find` glued to a shell metacharacter with
|
|
717
|
+
* no whitespace is still caught (issue #2034 review finding SEC-1/SEC-2).
|
|
718
|
+
*
|
|
719
|
+
* For each `find` token found, the search root is taken as the first
|
|
720
|
+
* following token that is not itself a `find` option (an option either
|
|
721
|
+
* starts with `-` or is `!` — `find`'s own root argument is always a bare
|
|
722
|
+
* positional path, never flag-shaped; the grouping operators `(`/`)` are
|
|
723
|
+
* shell metacharacters already stripped out by `extractLogicalTokens()`).
|
|
724
|
+
* Only an EXACT match against `FIND_ROOT_TOKEN_RE` blocks — `find
|
|
725
|
+
* /c/Users/.../repo -maxdepth 2 -name x` and `find "$REPO_PATH" ...` are
|
|
726
|
+
* legitimate, scoped searches and must be allowed.
|
|
727
|
+
*
|
|
728
|
+
* No operator override exists for this rule (unlike Rules 4/6-attribution)
|
|
729
|
+
* — a root-anchored `find` has no legitimate use in any ForgeDock pipeline.
|
|
730
|
+
*
|
|
731
|
+
* @param {string} command
|
|
732
|
+
* @returns {string|null} Error message to show, or null if allowed.
|
|
733
|
+
*/
|
|
734
|
+
function checkFindRoot(command) {
|
|
735
|
+
if (!command) return null;
|
|
736
|
+
// Cheap pre-filter to skip tokenization for the common case of a command
|
|
737
|
+
// with no `find` anywhere. Quote characters AND backslashes are stripped
|
|
738
|
+
// first so a degenerate/empty quote pair (`"f"ind`) or a backslash-escape
|
|
739
|
+
// (`f\ind` — real bash for the plain word `find`) glued inside the word
|
|
740
|
+
// itself doesn't break the substring match and cause a false "no find
|
|
741
|
+
// here" short-circuit that skips tokenization entirely (issue #2059,
|
|
742
|
+
// including the backslash-escape variant found in that issue's review).
|
|
743
|
+
if (!/find/i.test(command.replace(/["'\\]/g, ""))) return null;
|
|
744
|
+
|
|
745
|
+
const tokens = extractLogicalTokens(command);
|
|
746
|
+
|
|
747
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
748
|
+
if (tokens[i].toLowerCase() !== "find") continue;
|
|
749
|
+
|
|
750
|
+
// The search root is the first token after `find` that isn't a `find`
|
|
751
|
+
// option. `find` options always start with `-` (e.g. -maxdepth, -iname)
|
|
752
|
+
// or are `!` — the root path is always a bare positional token.
|
|
753
|
+
let rootToken = null;
|
|
754
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
755
|
+
const val = tokens[j];
|
|
756
|
+
if (val.startsWith("-") || val === "!") continue;
|
|
757
|
+
rootToken = val;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (rootToken && FIND_ROOT_TOKEN_RE.test(rootToken)) {
|
|
762
|
+
return [
|
|
763
|
+
`[ForgeDock] BLOCKED: \`find\` rooted at filesystem root "${rootToken}".`,
|
|
764
|
+
``,
|
|
765
|
+
`A \`find\` search starting at "${rootToken}" scans every mounted drive on`,
|
|
766
|
+
`Windows and never terminates — it becomes an orphaned, CPU-exhausting`,
|
|
767
|
+
`process once the parent sub-agent exits (issue #2034).`,
|
|
768
|
+
``,
|
|
769
|
+
`Fix: scope the search to $REPO_PATH or a known path, e.g.:`,
|
|
770
|
+
` find "$REPO_PATH" -iname "<pattern>"`,
|
|
771
|
+
``,
|
|
772
|
+
`No override exists for this rule — a root-anchored \`find\` has no`,
|
|
773
|
+
`legitimate use in any ForgeDock pipeline.`,
|
|
774
|
+
].join("\n");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// Rule 6: Attribution guard
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Check whether a commit/PR/issue command carries Claude Code default
|
|
787
|
+
* attribution. Blocks it so ForgeDock-branded pipeline output never inherits
|
|
788
|
+
* the assistant-tool signature.
|
|
789
|
+
*
|
|
790
|
+
* Override: FORGE_ALLOW_AI_ATTRIBUTION=1 in the operator's shell environment
|
|
791
|
+
* (read at hook startup, not from the tool payload — agents cannot set it via
|
|
792
|
+
* a Bash tool call in the same session).
|
|
793
|
+
*
|
|
794
|
+
* @param {string} command
|
|
795
|
+
* @returns {string|null} Error message to show, or null if allowed.
|
|
796
|
+
*/
|
|
797
|
+
function checkAttribution(command) {
|
|
798
|
+
// Operator override.
|
|
799
|
+
if (process.env.FORGE_ALLOW_AI_ATTRIBUTION === "1") return null;
|
|
800
|
+
|
|
801
|
+
// Only scan commands that persist a message/body to history.
|
|
802
|
+
if (!ATTRIBUTION_SCOPED_COMMANDS.some((re) => re.test(command))) return null;
|
|
803
|
+
|
|
804
|
+
const hit = AI_ATTRIBUTION_MARKERS.find((re) => re.test(command));
|
|
805
|
+
if (!hit) return null;
|
|
806
|
+
|
|
807
|
+
return [
|
|
808
|
+
`[ForgeDock] BLOCKED: Claude Code default attribution in a commit/PR/issue.`,
|
|
809
|
+
``,
|
|
810
|
+
`The command carries the assistant-tool attribution ("🤖 Generated with`,
|
|
811
|
+
`Claude Code" / "Co-Authored-By: Claude" / noreply@anthropic.com). Pipeline`,
|
|
812
|
+
`output is ForgeDock-branded — this must never land in the repo's public`,
|
|
813
|
+
`commit, PR, or issue history.`,
|
|
814
|
+
``,
|
|
815
|
+
`Fix: remove the attribution line/trailer. If you want a brand footer, use`,
|
|
816
|
+
`the ForgeDock signature instead:`,
|
|
817
|
+
` ${FORGEDOCK_SIGNATURE}`,
|
|
818
|
+
``,
|
|
819
|
+
`Exception: set FORGE_ALLOW_AI_ATTRIBUTION=1 in your shell environment BEFORE`,
|
|
820
|
+
`starting Claude Code if you intentionally need the assistant attribution.`,
|
|
821
|
+
].join("\n");
|
|
822
|
+
}
|
|
823
|
+
|
|
511
824
|
// ---------------------------------------------------------------------------
|
|
512
825
|
// Git helpers
|
|
513
826
|
// ---------------------------------------------------------------------------
|
|
@@ -552,10 +865,14 @@ function currentGitBranch() {
|
|
|
552
865
|
* quoted" is the wrong discriminator.
|
|
553
866
|
*
|
|
554
867
|
* This is intentionally NOT a full POSIX shell parser — it doesn't handle
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
868
|
+
* `$()`, backticks, or command chaining. It DOES handle backslash-escapes
|
|
869
|
+
* outside quotes (`\X` collapses to the literal character `X`, matching real
|
|
870
|
+
* bash semantics) — added for issue #2059 to close a `find`-guard bypass
|
|
871
|
+
* where `f\ind` tokenized with a literal backslash byte and never
|
|
872
|
+
* exact-matched `"find"`. It only needs to be accurate enough to distinguish
|
|
873
|
+
* "a flag in argument position" from "flag-shaped text embedded inside a
|
|
874
|
+
* different argument's value", which is all `extractFlag` needs (issue
|
|
875
|
+
* #1519), and to recover `find` as its own token regardless of escaping.
|
|
559
876
|
*
|
|
560
877
|
* @param {string} command
|
|
561
878
|
* @returns {CommandToken[]}
|
|
@@ -582,6 +899,24 @@ function tokenizeCommand(command) {
|
|
|
582
899
|
continue;
|
|
583
900
|
}
|
|
584
901
|
|
|
902
|
+
// Backslash-escape (outside quotes only — matches real bash semantics for
|
|
903
|
+
// `\X` when not already inside single/double quotes). A backslash strips
|
|
904
|
+
// the special meaning of the following character and the pair collapses
|
|
905
|
+
// to that character literally: `f\ind` is the plain word `find` to a real
|
|
906
|
+
// shell. Without this, `extractLogicalTokens()`'s exact-match check
|
|
907
|
+
// against `"find"` never fires because the token still contains a literal
|
|
908
|
+
// backslash byte (issue #2059 review finding — CONFIRMED HIGH). If the
|
|
909
|
+
// escaped character is whitespace, treat the token as `quoted` (glued
|
|
910
|
+
// words), mirroring the embedded-whitespace decoy-protection discriminator
|
|
911
|
+
// used elsewhere in this file for real quoting (issues #1519, #1591).
|
|
912
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
913
|
+
const next = command[i + 1];
|
|
914
|
+
current += next;
|
|
915
|
+
if (/\s/.test(next)) quoted = true;
|
|
916
|
+
i++; // consume the escaped character; the for-loop's own increment moves past it
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
585
920
|
if (ch === "'") {
|
|
586
921
|
inSingle = true;
|
|
587
922
|
quoted = true;
|
package/bin/init-enrich-api.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* init-enrich-api.mjs — Anthropic API enrichment backend for ForgeDock init.
|
|
3
3
|
*
|
|
4
|
-
* Implements the same enrich(ConfigDraft) contract as the
|
|
5
|
-
* selection ladder in
|
|
4
|
+
* Implements the same enrich(ConfigDraft) contract as the cli backend
|
|
5
|
+
* (bin/init-enrich-cli.mjs) so the selection ladder in bin/init-enrich.mjs
|
|
6
|
+
* can treat both backends interchangeably.
|
|
6
7
|
*
|
|
7
8
|
* Uses Node.js built-in fetch (Node 18+) — no SDK dependency required.
|
|
8
9
|
* Reads ANTHROPIC_API_KEY from the environment; cleanly skips enrichment with
|
|
@@ -153,10 +154,17 @@ export function parseEnrichedDraft(output, draft) {
|
|
|
153
154
|
* baseline.
|
|
154
155
|
*
|
|
155
156
|
* @param {object} draft - ConfigDraft from detectConfig()
|
|
157
|
+
* @param {object} [opts]
|
|
158
|
+
* @param {object} [opts.env] - Environment to read ANTHROPIC_API_KEY from.
|
|
159
|
+
* Defaults to process.env. Injectable so callers (bin/init-enrich.mjs's
|
|
160
|
+
* dispatcher, which itself receives it from bin/journey.mjs's ctx.env) can
|
|
161
|
+
* exercise this backend deterministically in tests without depending on
|
|
162
|
+
* the real process environment.
|
|
156
163
|
* @returns {Promise<object>} Enriched ConfigDraft, or the original draft on failure
|
|
157
164
|
*/
|
|
158
|
-
export async function enrich(draft) {
|
|
159
|
-
const
|
|
165
|
+
export async function enrich(draft, opts = {}) {
|
|
166
|
+
const { env = process.env } = opts;
|
|
167
|
+
const apiKey = env.ANTHROPIC_API_KEY;
|
|
160
168
|
|
|
161
169
|
if (!apiKey) {
|
|
162
170
|
console.error(
|