forgedock 1.0.7 → 1.0.9

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.
@@ -824,9 +824,9 @@ All tool commands are read from `forge.yaml → verification.commands`; each ste
824
824
 
825
825
  ```bash
826
826
  # Read toolchain commands from forge.yaml
827
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
828
- TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
829
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
827
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
828
+ TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
829
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
830
830
 
831
831
  # Check if this is a milestone batch with TypeScript changes
832
832
  MILESTONE_BRANCH="milestone/{milestone_slug}"
@@ -14,6 +14,22 @@ Performs comprehensive review of `staging` before merging to `main`. Handles lar
14
14
 
15
15
  ---
16
16
 
17
+ ## Config Resolution
18
+
19
+ Read `forge.yaml` to resolve branch names before running any commands:
20
+
21
+ ```bash
22
+ CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
23
+ GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
24
+ GH_FLAG="-R $GH_REPO"
25
+ DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
26
+ STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
27
+ ```
28
+
29
+ All `$DEFAULT_BRANCH` and `$STAGING_BRANCH` references below are populated from `forge.yaml`.
30
+
31
+ ---
32
+
17
33
  ## Evidence-Based Review Protocol (ALL Agents)
18
34
 
19
35
  ### Diff-First Approach
@@ -70,22 +86,6 @@ Include ALL findings (CONFIRMED, LIKELY, POSSIBLE). One line per finding, sequen
70
86
 
71
87
  ---
72
88
 
73
- ## Config Resolution
74
-
75
- Read `forge.yaml` to resolve branch names before running any commands:
76
-
77
- ```bash
78
- CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
79
- GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
80
- GH_FLAG="-R $GH_REPO"
81
- DEFAULT_BRANCH=$(yq '.branches.default' "$CONFIG_FILE")
82
- STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
83
- ```
84
-
85
- All `$DEFAULT_BRANCH` and `$STAGING_BRANCH` references below are populated from `forge.yaml`.
86
-
87
- ---
88
-
89
89
  ## Phase 0A: Open Review-Finding Gate (BLOCKING — runs before scope analysis)
90
90
 
91
91
  **Purpose**: Prevent deploying commits that have known, unfixed review findings. The review system catches bugs before merging; this gate ensures the merge path acts on that information.
@@ -213,8 +213,8 @@ Create review chunks by priority: Billing/Pricing (CRITICAL) → Security/Auth (
213
213
  Read `forge.yaml → verification.commands.python` for project-specific tool commands:
214
214
 
215
215
  ```bash
216
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
217
- PYTHON_LINT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'lint:' | head -1 | sed "s/.*lint: *['\"]//;s/['\"].*//")
216
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
217
+ PYTHON_LINT=$(yq '.verification.commands.python.lint // ""' forge.yaml 2>/dev/null || echo '')
218
218
 
219
219
  if [ -n "$PYTHON_FORMAT" ]; then
220
220
  eval "$PYTHON_FORMAT" 2>&1 | head -30
@@ -234,8 +234,8 @@ fi
234
234
  Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
235
235
 
236
236
  ```bash
237
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
238
- TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
237
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
238
+ TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
239
239
 
240
240
  if [ -n "$TS_TYPECHECK" ]; then
241
241
  eval "$TS_TYPECHECK" 2>&1
@@ -261,7 +261,7 @@ Build failure is BLOCKING — deploy WILL fail. Typecheck alone misses SSG/prere
261
261
  Read `forge.yaml → verification.commands.python.test`:
262
262
 
263
263
  ```bash
264
- PYTHON_TEST=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
264
+ PYTHON_TEST=$(yq '.verification.commands.python.test // ""' forge.yaml 2>/dev/null || echo '')
265
265
 
266
266
  if [ -n "$PYTHON_TEST" ]; then
267
267
  eval "$PYTHON_TEST" 2>&1 | tail -50
@@ -161,9 +161,9 @@ Read `forge.yaml → verification.commands.python` for project-specific tool com
161
161
 
162
162
  ```bash
163
163
  # Read toolchain commands from forge.yaml
164
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
165
- PYTHON_LINT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'lint:' | head -1 | sed "s/.*lint: *['\"]//;s/['\"].*//")
166
- PYTHON_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
164
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
165
+ PYTHON_LINT=$(yq '.verification.commands.python.lint // ""' forge.yaml 2>/dev/null || echo '')
166
+ PYTHON_TYPECHECK=$(yq '.verification.commands.python.typecheck // ""' forge.yaml 2>/dev/null || echo '')
167
167
 
168
168
  # Run format check
169
169
  if [ -n "$PYTHON_FORMAT" ]; then
@@ -187,7 +187,7 @@ else
187
187
  fi
188
188
 
189
189
  # Always run compile check on changed Python files (fast, language-universal)
190
- for f in $(gh pr diff $ARGUMENTS --name-only | grep '\.py$'); do
190
+ gh pr diff $ARGUMENTS --name-only | grep '\.py$' | while IFS= read -r f; do
191
191
  python3 -m py_compile "$f" 2>&1
192
192
  done
193
193
  ```
@@ -197,9 +197,9 @@ done
197
197
  Read `forge.yaml → verification.commands.typescript` for project-specific tool commands:
198
198
 
199
199
  ```bash
200
- TS_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
201
- TS_LINT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'lint:' | head -1 | sed "s/.*lint: *['\"]//;s/['\"].*//")
202
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
200
+ TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
201
+ TS_LINT=$(yq '.verification.commands.typescript.lint // ""' forge.yaml 2>/dev/null || echo '')
202
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
203
203
 
204
204
  # Run format check
205
205
  if [ -n "$TS_FORMAT" ]; then
@@ -243,7 +243,7 @@ gh pr diff $ARGUMENTS | grep -oE "['\"][A-Za-z0-9+/=]{40,}['\"]" | head -10
243
243
 
244
244
  ### 2F: SQL Migration Validation (if *.sql changed)
245
245
  ```bash
246
- for sql_file in $(gh pr diff $ARGUMENTS --name-only | grep "\.sql$"); do
246
+ gh pr diff $ARGUMENTS --name-only | grep "\.sql$" | while IFS= read -r sql_file; do
247
247
  grep -E "FOR UPDATE" "$sql_file" | grep -qE "(SUM|COUNT|AVG|MIN|MAX)\s*\(" && echo "ERROR: FOR UPDATE with aggregate"
248
248
  grep -qE "DROP (TABLE|COLUMN|INDEX)" "$sql_file" && ! grep -qE "IF EXISTS" "$sql_file" && echo "WARNING: DROP without IF EXISTS"
249
249
  grep -qE "ALTER TABLE.*ADD COLUMN.*NOT NULL" "$sql_file" && ! grep -qE "DEFAULT" "$sql_file" && echo "WARNING: NOT NULL without DEFAULT"
@@ -263,7 +263,7 @@ Read `forge.yaml → verification.commands.{lang}.test` for the project's test c
263
263
  ```bash
264
264
  # Check each configured language for a test command
265
265
  for lang in python typescript go rust; do
266
- TEST_CMD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk "/^ ${lang}:/{f=1;next} f && /^ [^ \t]/{exit} f" | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
266
+ TEST_CMD=$(yq ".verification.commands.${lang}.test // \"\"" forge.yaml 2>/dev/null || echo '')
267
267
  if [ -n "$TEST_CMD" ]; then
268
268
  echo "=== Running tests (${lang}): ${TEST_CMD} ==="
269
269
  eval "$TEST_CMD" 2>&1 | tail -30
@@ -273,8 +273,8 @@ for lang in python typescript go rust; do
273
273
  done
274
274
 
275
275
  # If no test commands were configured, log explicitly
276
- PYTHON_TEST=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
277
- TS_TEST=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'test:' | head -1 | sed "s/.*test: *['\"]//;s/['\"].*//")
276
+ PYTHON_TEST=$(yq '.verification.commands.python.test // ""' forge.yaml 2>/dev/null || echo '')
277
+ TS_TEST=$(yq '.verification.commands.typescript.test // ""' forge.yaml 2>/dev/null || echo '')
278
278
  if [ -z "$PYTHON_TEST" ] && [ -z "$TS_TEST" ]; then
279
279
  echo "SKIPPED — no test commands configured in verification.commands"
280
280
  fi
@@ -303,8 +303,8 @@ Read `forge.yaml → verification.commands.typescript.typecheck` and `.build`:
303
303
  ```bash
304
304
  gh pr checkout $ARGUMENTS --detach 2>/dev/null
305
305
 
306
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
307
- TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
306
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
307
+ TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
308
308
 
309
309
  if [ -n "$TS_TYPECHECK" ]; then
310
310
  eval "$TS_TYPECHECK" 2>&1
@@ -337,10 +337,10 @@ Read `forge.yaml → verification.commands.python.format` and `.build`:
337
337
  gh pr checkout $ARGUMENTS --detach 2>/dev/null
338
338
 
339
339
  # Compile-check all changed Python files (language-universal — no config needed)
340
- for f in $(echo "$CHANGED_FILES" | grep '\.py$'); do python3 -m py_compile "$f" 2>&1; done
340
+ echo "$CHANGED_FILES" | grep '\.py$' | while IFS= read -r f; do python3 -m py_compile "$f" 2>&1; done
341
341
 
342
342
  if [ "$REQUIRES_FULL_BUILD" = "true" ]; then
343
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
343
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
344
344
  if [ -n "$PYTHON_FORMAT" ]; then
345
345
  eval "$PYTHON_FORMAT" 2>&1
346
346
  else
@@ -495,7 +495,7 @@ fi
495
495
  # Python scoping hazard check — local imports that shadow module-level names
496
496
  # A local `import X` makes X a local variable for the ENTIRE function scope.
497
497
  # Any reference to X ABOVE the local import will crash with UnboundLocalError.
498
- for f in $(echo "$CHANGED_FILES" | grep -E '\.py$'); do
498
+ echo "$CHANGED_FILES" | grep -E '\.py$' | while IFS= read -r f; do
499
499
  echo "=== Python Scoping Check: $f ==="
500
500
  # Find function-scoped imports (indented import statements)
501
501
  grep -nE "^\s+import [a-z]" "$f" 2>/dev/null | while read line; do
@@ -509,7 +509,7 @@ for f in $(echo "$CHANGED_FILES" | grep -E '\.py$'); do
509
509
  done
510
510
 
511
511
  # Config file assumption check (baked into Docker image vs volume-mounted)
512
- for f in $(echo "$CHANGED_FILES" | grep -E "config/.*\.(json|yaml|yml)$"); do
512
+ echo "$CHANGED_FILES" | grep -E "config/.*\.(json|yaml|yml)$" | while IFS= read -r f; do
513
513
  echo "=== Config File: $f ==="
514
514
  grep -n "$(dirname $f)" docker-compose.yml 2>/dev/null || echo "WARNING: Config dir may not be mounted — changes may require --build"
515
515
  grep -n "$(dirname $f)" services/*/Dockerfile 2>/dev/null || true
@@ -122,7 +122,7 @@ for f in {reverted_python_files}; do
122
122
  done
123
123
 
124
124
  # Python format check (config-driven)
125
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
125
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
126
126
  if [ -n "$PYTHON_FORMAT" ]; then
127
127
  eval "$PYTHON_FORMAT" 2>&1 | head -20
128
128
  else
@@ -130,7 +130,7 @@ else
130
130
  fi
131
131
 
132
132
  # TypeScript typecheck (config-driven)
133
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
133
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
134
134
  if [ -n "$TS_TYPECHECK" ]; then
135
135
  eval "$TS_TYPECHECK" 2>&1 | head -20
136
136
  else
@@ -94,7 +94,7 @@ Run after quality gate passes. All tool commands are read from `forge.yaml → v
94
94
  ```bash
95
95
  cd {WORKTREE_PATH}
96
96
 
97
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
97
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
98
98
  if [ -n "$PYTHON_FORMAT" ]; then
99
99
  eval "$PYTHON_FORMAT" 2>&1
100
100
  else
@@ -110,9 +110,9 @@ Failures in `py_compile` are BLOCKING — fix before continuing.
110
110
  ```bash
111
111
  cd {WORKTREE_PATH}
112
112
 
113
- TS_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
114
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
115
- TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
113
+ TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
114
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
115
+ TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
116
116
 
117
117
  if [ -n "$TS_FORMAT" ]; then
118
118
  eval "$TS_FORMAT" 2>&1
@@ -214,7 +214,24 @@ Fix HIGH/MEDIUM findings. Max 2 iterations. Skip for 1-file config/docs edits.
214
214
  All client-side fetch/useSWR/apiFetch must use `/api/...` proxy routes, NEVER `/api/v1/...` directly.
215
215
 
216
216
  ### 3I: Deployment completeness check (MANDATORY)
217
- New env vars must be in: `.env.example` + SOPS `prod.enc.yaml` + `decrypt-secrets.sh` ENV_MAPPING + `env_validation.py` (if API).
217
+ Skip if no new env vars introduced.
218
+
219
+ For each new env var, verify present in ALL required locations:
220
+
221
+ | Location | Required for |
222
+ |----------|-------------|
223
+ | `.env.example` | All new vars |
224
+ | Secrets backend (see `deploy.secrets_backend`) | Secret vars — skip if backend is `none` or unset |
225
+ | `app/env_validation.py` | API service vars (if project has one) |
226
+
227
+ **Secrets backend check** *(trigger: `deploy.secrets_backend == "sops"`)*:
228
+
229
+ If the project uses SOPS, verify the new var is present in all SOPS chain locations:
230
+ - `infra/secrets/prod.enc.yaml` — SOPS-encrypted secret store
231
+ - `infra/decrypt-secrets.sh` ENV_MAPPING — maps SOPS key to env var name
232
+
233
+ If `deploy.secrets_backend` is absent or not `sops`, skip these checks and log:
234
+ > `SKIP: SOPS chain check — deploy.secrets_backend is not "sops". Configure deploy.secrets_backend in forge.yaml to enable.`
218
235
 
219
236
  ### 3J: Commit
220
237
  Conventional prefix (fix/feat/refactor/docs). Reference #{NUMBER} in the commit message.
@@ -753,7 +753,7 @@ All tool commands are read from `forge.yaml → verification.commands`. When a k
753
753
  ```bash
754
754
  cd {WORKTREE_PATH}
755
755
 
756
- PYTHON_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ python:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
756
+ PYTHON_FORMAT=$(yq '.verification.commands.python.format // ""' forge.yaml 2>/dev/null || echo '')
757
757
  if [ -n "$PYTHON_FORMAT" ]; then
758
758
  eval "$PYTHON_FORMAT" 2>&1
759
759
  else
@@ -769,9 +769,9 @@ python -m py_compile {PYTHON_FILES}
769
769
  ```bash
770
770
  cd {WORKTREE_PATH}
771
771
 
772
- TS_FORMAT=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'format:' | head -1 | sed "s/.*format: *['\"]//;s/['\"].*//")
773
- TS_TYPECHECK=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'typecheck:' | head -1 | sed "s/.*typecheck: *['\"]//;s/['\"].*//")
774
- TS_BUILD=$(awk '/^ commands:/{f=1;next} f && /^[^ \t]/{exit} f' forge.yaml 2>/dev/null | awk '/^ typescript:/{f=1;next} f && /^ [^ \t]/{exit} f' | grep 'build:' | head -1 | sed "s/.*build: *['\"]//;s/['\"].*//")
772
+ TS_FORMAT=$(yq '.verification.commands.typescript.format // ""' forge.yaml 2>/dev/null || echo '')
773
+ TS_TYPECHECK=$(yq '.verification.commands.typescript.typecheck // ""' forge.yaml 2>/dev/null || echo '')
774
+ TS_BUILD=$(yq '.verification.commands.typescript.build // ""' forge.yaml 2>/dev/null || echo '')
775
775
 
776
776
  if [ -n "$TS_FORMAT" ]; then
777
777
  eval "$TS_FORMAT" 2>&1
package/docs/CONFIG.md CHANGED
@@ -12,6 +12,21 @@ echo "forge.yaml" >> .gitignore # if your credentials path is sensitive
12
12
 
13
13
  ---
14
14
 
15
+ ## CLI Flags for `init`
16
+
17
+ `npx forgedock init` accepts two optional flags that change how configuration is generated:
18
+
19
+ | Flag | Behavior |
20
+ |------|----------|
21
+ | `--manual` | Skips AI autopilot enrichment entirely. Presents the annotated review screen with detection baseline values only (no AI-enriched suggestions). Detection still runs to provide defaults — you review and edit the raw inferred values on the same single-screen interface. |
22
+ | `--verbose` | Surfaces each field's detection source and confidence rationale during the init flow. Helpful for understanding where ForgeDock inferred values from (e.g. `git remote`, `package.json`, `gh api`). |
23
+
24
+ The two flags can be combined: `npx forgedock init --manual --verbose` shows the annotated review screen with detection baseline values and field provenance at each entry.
25
+
26
+ The default (`npx forgedock init` with no flags) remains the zero-question autopilot: AI enrichment fills in everything it can, and you review the result on a single annotated screen.
27
+
28
+ ---
29
+
15
30
  ## Schema Overview
16
31
 
17
32
  | Section | Required | Purpose |
@@ -468,6 +483,96 @@ Do **not** mix the two patterns for the same block of variables (e.g., `yq` with
468
483
 
469
484
  ---
470
485
 
486
+ ## ConfigDraft Contract
487
+
488
+ `ConfigDraft` is the shared data structure emitted by `bin/init-detect.mjs` and consumed by the AI enrichment backends (`init-enrich`) and the annotated review renderer (`review-render`). It mirrors the required sections of `forge.yaml` at a per-field granularity, adding provenance and confidence metadata.
489
+
490
+ ### Field shape
491
+
492
+ Every leaf value in a `ConfigDraft` is a **ConfigField** object:
493
+
494
+ ```ts
495
+ {
496
+ value: string, // The detected or inferred value
497
+ confidence: "high" | "medium" | "low", // How certain the detection was
498
+ source: string, // Human-readable label for where the value came from
499
+ why: string, // Plain-language explanation of why this value was chosen
500
+ }
501
+ ```
502
+
503
+ **Confidence levels:**
504
+
505
+ | Level | Meaning |
506
+ |-------|---------|
507
+ | `"high"` | Verified from a concrete, unambiguous source (e.g., parsed from the git remote URL) |
508
+ | `"medium"` | Inferred from available signals; likely correct but not guaranteed (e.g., current branch name, name derived from repo slug) |
509
+ | `"low"` | Guessed default — no supporting evidence was found (e.g., no git remote, not a git repo) |
510
+
511
+ ### ConfigDraft shape
512
+
513
+ ```js
514
+ {
515
+ project: {
516
+ owner: ConfigField, // GitHub org or username
517
+ repo: ConfigField, // Repository name (without owner prefix)
518
+ name: ConfigField, // Human-readable project name
519
+ },
520
+ paths: {
521
+ root: ConfigField, // Absolute path to the project root
522
+ worktreeBase: ConfigField, // Absolute path to the git worktree base dir
523
+ },
524
+ branches: {
525
+ default: ConfigField, // Default branch (e.g. "main")
526
+ staging: ConfigField, // Staging branch for fast-lane PRs
527
+ },
528
+ meta: {
529
+ remoteDetected: boolean, // true iff a parseable git remote URL was found
530
+ },
531
+ }
532
+ ```
533
+
534
+ ### Example output (high-confidence repo)
535
+
536
+ ```js
537
+ {
538
+ project: {
539
+ owner: { value: "acme-org", confidence: "high", source: "git remote origin (SSH)", why: "Parsed from SSH remote URL: git@github.com:acme-org/acme-platform.git" },
540
+ repo: { value: "acme-platform", confidence: "high", source: "git remote origin (SSH)", why: "Parsed from SSH remote URL: git@github.com:acme-org/acme-platform.git" },
541
+ name: { value: "Acme Platform", confidence: "medium", source: "derived from repo slug", why: "Title-cased version of repo name 'acme-platform' (split on hyphens/underscores)" },
542
+ },
543
+ paths: {
544
+ root: { value: "/home/user/acme", confidence: "high", source: "process.cwd()", why: "Absolute path passed to detectConfig — the project root" },
545
+ worktreeBase: { value: "/home/user/acme/.claude/worktrees", confidence: "high", source: "derived from root", why: "Convention: {root}/.claude/worktrees" },
546
+ },
547
+ branches: {
548
+ default: { value: "main", confidence: "high", source: "git symbolic-ref refs/remotes/origin/HEAD", why: "Remote HEAD points to main" },
549
+ staging: { value: "staging", confidence: "high", source: "git branch -r", why: "Found 'origin/staging' in the remote branch listing" },
550
+ },
551
+ meta: { remoteDetected: true },
552
+ }
553
+ ```
554
+
555
+ ### Producing a ConfigDraft
556
+
557
+ ```js
558
+ import { detectConfig } from "./bin/init-detect.mjs";
559
+
560
+ const draft = await detectConfig(process.cwd());
561
+ // draft.project.owner.value → "acme-org"
562
+ // draft.project.owner.confidence → "high"
563
+ ```
564
+
565
+ `detectConfig(cwd)` is the sole public API. It never throws — every error path produces a `low`-confidence default.
566
+
567
+ ### Consuming a ConfigDraft
568
+
569
+ Downstream consumers read `field.value` for the raw value and `field.confidence` to decide how to handle it:
570
+
571
+ - **`init-enrich`** (AI enrichment): passes `low`- and `medium`-confidence fields to the AI backend to raise their confidence; leaves `high`-confidence fields untouched.
572
+ - **`review-render`** (TUI review screen): shows each field with its source and why; highlights `low`-confidence fields with a `# TODO(forgedock:<field>)` annotation in the generated YAML.
573
+
574
+ ---
575
+
471
576
  ## CLAUDE.md Integration
472
577
 
473
578
  ForgeDock can inject a managed usage block into your project's `CLAUDE.md` so every Claude Code session opened in the repo automatically knows that ForgeDock drives development here and which commands to use.
@@ -510,6 +615,68 @@ npx forgedock integrate
510
615
 
511
616
  ---
512
617
 
618
+ ## Per-Directory State Registry
619
+
620
+ ForgeDock tracks per-directory opt-out state in a central registry file on the local machine.
621
+
622
+ ### Registry File Location
623
+
624
+ ```
625
+ ~/.claude/forgedock/registry.json
626
+ ```
627
+
628
+ The directory (`~/.claude/forgedock/`) is created automatically on first use with mode `0700`. The file is never committed to version control — it is per-user, per-machine state.
629
+
630
+ ### Registry Schema
631
+
632
+ ```json
633
+ {
634
+ "version": 1,
635
+ "optedOut": {
636
+ "/absolute/path/to/project": { "at": "2026-06-09T12:00:00.000Z" }
637
+ },
638
+ "nudgeSeen": {
639
+ "/absolute/path/to/project": { "at": "2026-06-09T12:00:00.000Z" }
640
+ }
641
+ }
642
+ ```
643
+
644
+ | Field | Type | Description |
645
+ |-------|------|-------------|
646
+ | `version` | number | Schema version — currently `1` |
647
+ | `optedOut` | object | Map of absolute directory paths to opt-out metadata |
648
+ | `optedOut[path].at` | string | ISO-8601 timestamp of when the opt-out was recorded |
649
+ | `nudgeSeen` | object | Map of absolute directory paths where the one-time "Enable ForgeDock here?" nudge has already been shown |
650
+ | `nudgeSeen[path].at` | string | ISO-8601 timestamp of when the nudge was shown for this directory |
651
+
652
+ ### State Resolution
653
+
654
+ The `registry` module resolves one of three states for any directory:
655
+
656
+ | State | Meaning |
657
+ |-------|---------|
658
+ | `managed-active` | Directory contains `forge.yaml` or `.forgedock` and is **not** opted out — ForgeDock is active here |
659
+ | `managed-optedout` | Directory has a managed marker but the user has explicitly opted out — ForgeDock stays silent |
660
+ | `unmanaged` | No `forge.yaml` or `.forgedock` marker found — ForgeDock has no presence here |
661
+
662
+ **Opt-out wins over managed**: if a directory contains `forge.yaml` but its path is listed in `optedOut`, the state is `managed-optedout`.
663
+
664
+ ### Failure Behaviour
665
+
666
+ A missing or corrupt `registry.json` is treated as an empty opt-out set. The registry always fails open — it never throws and never blocks a Claude Code session.
667
+
668
+ ### Managing Opt-Out State
669
+
670
+ Use the `forgedock enable` and `forgedock disable` commands to add or remove a directory from the opt-out set:
671
+
672
+ ```bash
673
+ npx forgedock enable [dir] # Remove directory from opt-out set (default: cwd)
674
+ npx forgedock disable [dir] # Add directory to opt-out set (default: cwd)
675
+ npx forgedock status [dir] # Show resolved state for a directory
676
+ ```
677
+
678
+ ---
679
+
513
680
  ## Complete Example
514
681
 
515
682
  See [`forge.yaml.example`](../forge.yaml.example) at the repository root for a fully annotated example covering all sections.