forgedock 1.0.22 → 1.1.2

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 (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ # issue-dedup.sh — Deterministic near-duplicate check for GitHub issue creation
3
+ #
4
+ # Usage:
5
+ # issue-dedup.sh <title> [-R <owner/repo>] [--force]
6
+ # issue-dedup.sh <title> ["-R <owner/repo>"] [--force] (single pre-joined token also accepted)
7
+ #
8
+ # <title> : Proposed issue title (required, must be quoted if it contains spaces)
9
+ # -R <owner/repo> : GitHub repository, as two argv tokens (optional, defaults to
10
+ # FORGE_REPO or current repo)
11
+ # "-R <owner/repo>" : GitHub repository, as ONE pre-joined token (e.g. when a caller
12
+ # passes an already-composed $GH_FLAG variable quoted as "$GH_FLAG").
13
+ # Both forms are accepted. A title that happens to start with "-R "
14
+ # but lacks the "/" separator (e.g. "-R login is broken") is
15
+ # correctly detected by the parser and treated as a title, not a
16
+ # repo flag. See the -R\ * case arm guard in the arg-parsing loop.
17
+ # --force : Skip dedup check and always allow creation (explicit override)
18
+ #
19
+ # Exit codes:
20
+ # 0 — No near-duplicate found. Creation is safe. Stdout is empty.
21
+ # 1 — Near-duplicate found. Stdout contains the matched issue number and title.
22
+ # Callers MUST check exit code before running `gh issue create`.
23
+ # 2 — Usage error (missing required argument).
24
+ #
25
+ # Algorithm (deterministic — no LLM required):
26
+ # 1. Normalize the proposed title: lowercase, strip punctuation, split into tokens
27
+ # of length ≥ 4 (short words are noise: "fix", "add", "the", etc.)
28
+ # 2. Query open issues: search GitHub using the 2 most distinctive title tokens
29
+ # as the --search query (balances recall vs API cost)
30
+ # 3. For each candidate open issue, count shared tokens (length ≥ 4) between
31
+ # the proposed title and the candidate title.
32
+ # 4. MATCH threshold: ≥ 3 shared tokens OR shared token count ≥ 50% of
33
+ # the shorter title's token set — whichever fires first.
34
+ # 5. On match: print "DUPLICATE: #<N> <title>" to stdout, exit 1.
35
+ # 6. On no match: exit 0 (silent).
36
+ #
37
+ # Integration pattern (for command specs that call gh issue create):
38
+ #
39
+ # DEDUP_RESULT=$(scripts/issue-dedup.sh "$PROPOSED_TITLE" "$GH_FLAG" 2>&1)
40
+ # # ^ $GH_FLAG is typically already composed as "-R owner/repo" and quoted here,
41
+ # # producing ONE argv token — the parser accepts this joined form directly
42
+ # # (see the -R\ * case arm below). An unset/empty $GH_FLAG quotes to an empty
43
+ # # token, which the parser also treats as "no repo flag" rather than a title.
44
+ # DEDUP_EXIT=$?
45
+ # if [ $DEDUP_EXIT -eq 1 ]; then
46
+ # echo "Near-duplicate detected: $DEDUP_RESULT"
47
+ # echo "Comment on the existing issue instead of creating a new one."
48
+ # echo "Use --force to override and create anyway."
49
+ # exit 1
50
+ # elif [ $DEDUP_EXIT -eq 2 ]; then
51
+ # echo "Dedup check usage error: $DEDUP_RESULT"
52
+ # echo "Do NOT proceed to issue creation — fix the invocation and retry."
53
+ # exit 2
54
+ # fi
55
+ # gh issue create ... # only runs when exit 0
56
+ #
57
+ # Callers MUST explicitly branch on exit 2 (usage error) as a hard stop, not
58
+ # just exit 1 (duplicate found). Silently falling through on any non-1 exit
59
+ # code — including redirecting stderr to /dev/null — means a malformed
60
+ # invocation (bad flag, missing argument) is indistinguishable from "no
61
+ # duplicate found" and proceeds to create a possibly-duplicate issue with
62
+ # zero operator-visible signal.
63
+ #
64
+ # Notes:
65
+ # - Closed issues are NOT matched (a regression should be filed fresh with a
66
+ # "Regression of #N" reference; see issue.md Phase 2D).
67
+ # - The --force flag is the explicit override path. Human decision is required
68
+ # to use it — agents MUST NOT pass --force without user authorization.
69
+ # - Token matching is case-insensitive and punctuation-agnostic.
70
+ # - GitHub's --search flag performs a full-text search; the token query is a
71
+ # tiebreaker against the full candidate list, not the sole filter.
72
+ #
73
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
74
+ # SPDX-License-Identifier: AGPL-3.0-or-later
75
+
76
+ set -euo pipefail
77
+
78
+ # --------------------------------------------------------------------------- #
79
+ # Argument parsing
80
+ # --------------------------------------------------------------------------- #
81
+ PROPOSED_TITLE=""
82
+ GH_FLAG=""
83
+ FORCE=0
84
+
85
+ while [[ $# -gt 0 ]]; do
86
+ case "$1" in
87
+ "")
88
+ # Empty-string token — e.g. a caller quoting an unset/empty $GH_FLAG
89
+ # ("$GH_FLAG" -> ""). Treat as "no repo flag supplied", not as a title.
90
+ shift
91
+ ;;
92
+ -R)
93
+ shift
94
+ if [[ $# -eq 0 ]]; then
95
+ echo "Missing value for -R" >&2
96
+ exit 2
97
+ fi
98
+ GH_FLAG="-R $1"
99
+ shift
100
+ ;;
101
+ -R\ *)
102
+ # Single pre-joined token, e.g. "-R owner/repo" — produced when a caller
103
+ # quotes an already-composed $GH_FLAG variable ("$GH_FLAG") instead of
104
+ # passing -R and the repo as two separate argv tokens. Both call shapes
105
+ # are valid integration patterns used across commands/*.md callers.
106
+ #
107
+ # Guard: a valid repo token always contains '/' (owner/repo format).
108
+ # If the value after "-R " has no '/', the token is a positional title
109
+ # that happens to start with "-R " (e.g. "-R login is broken") — treat
110
+ # it as the title instead. This prevents argv-flag-title-collision (#1625).
111
+ _rval="${1#-R }"
112
+ if [[ "$_rval" == */* ]]; then
113
+ GH_FLAG="$1"
114
+ shift
115
+ else
116
+ PROPOSED_TITLE="$1"
117
+ shift
118
+ fi
119
+ ;;
120
+ --force)
121
+ FORCE=1
122
+ shift
123
+ ;;
124
+ -*)
125
+ echo "Unknown flag: $1" >&2
126
+ exit 2
127
+ ;;
128
+ *)
129
+ PROPOSED_TITLE="$1"
130
+ shift
131
+ ;;
132
+ esac
133
+ done
134
+
135
+ if [[ -z "$PROPOSED_TITLE" ]]; then
136
+ echo "Usage: issue-dedup.sh <title> [-R <owner/repo>] [--force]" >&2
137
+ exit 2
138
+ fi
139
+
140
+ # Force override — skip all checks
141
+ if [[ "$FORCE" -eq 1 ]]; then
142
+ exit 0
143
+ fi
144
+
145
+ # --------------------------------------------------------------------------- #
146
+ # Token normalization
147
+ # Lowercase, strip non-alphanumeric chars except spaces, split on whitespace,
148
+ # keep tokens of length >= 4 to filter out noise words.
149
+ # --------------------------------------------------------------------------- #
150
+ normalize_tokens() {
151
+ local text="$1"
152
+ echo "$text" \
153
+ | tr '[:upper:]' '[:lower:]' \
154
+ | sed 's/[^a-z0-9 ]/ /g' \
155
+ | tr -s ' ' '\n' \
156
+ | awk 'length >= 4' \
157
+ | sort -u
158
+ }
159
+
160
+ PROPOSED_TOKENS=$(normalize_tokens "$PROPOSED_TITLE")
161
+ # grep -c always prints a numeric count (including "0") even when it matches
162
+ # nothing — it just exits 1 in that case. `|| echo 0` would append a SECOND
163
+ # "0" line on top of grep's own "0" output, corrupting the numeric comparison
164
+ # below. `|| true` only suppresses the non-zero exit under `set -e`.
165
+ PROPOSED_TOKEN_COUNT=$(echo "$PROPOSED_TOKENS" | grep -c . || true)
166
+
167
+ if [[ "$PROPOSED_TOKEN_COUNT" -lt 1 ]]; then
168
+ # Title has no meaningful tokens — skip dedup (cannot match)
169
+ exit 0
170
+ fi
171
+
172
+ # Build a search query from the 2 most distinctive tokens (longest first)
173
+ SEARCH_TERMS=$(echo "$PROPOSED_TOKENS" | awk '{ print length, $0 }' | sort -rn | awk '{print $2}' | head -2 | tr '\n' ' ')
174
+ SEARCH_QUERY=$(echo "$SEARCH_TERMS" | xargs)
175
+
176
+ # --------------------------------------------------------------------------- #
177
+ # Query open issues
178
+ # --------------------------------------------------------------------------- #
179
+ CANDIDATES=$(gh issue list $GH_FLAG \
180
+ --state open \
181
+ --search "$SEARCH_QUERY" \
182
+ --limit 30 \
183
+ --json number,title \
184
+ --jq '.[] | "\(.number)\t\(.title)"' 2>/dev/null || true)
185
+
186
+ if [[ -z "$CANDIDATES" ]]; then
187
+ exit 0
188
+ fi
189
+
190
+ # --------------------------------------------------------------------------- #
191
+ # Token overlap matching
192
+ # For each candidate, count shared tokens between proposed and candidate titles.
193
+ # Match if: shared_count >= 3 OR shared_count >= 50% of the shorter token set.
194
+ # --------------------------------------------------------------------------- #
195
+ MATCH_NUMBER=""
196
+ MATCH_TITLE=""
197
+
198
+ while IFS=$'\t' read -r CAND_NUMBER CAND_TITLE; do
199
+ [[ -z "$CAND_NUMBER" ]] && continue
200
+
201
+ CAND_TOKENS=$(normalize_tokens "$CAND_TITLE")
202
+ # Same double-fallback hazard as PROPOSED_TOKEN_COUNT above — see comment there.
203
+ CAND_TOKEN_COUNT=$(echo "$CAND_TOKENS" | grep -c . || true)
204
+
205
+ # Shared token count: intersection of proposed and candidate token sets
206
+ SHARED_COUNT=$(comm -12 \
207
+ <(echo "$PROPOSED_TOKENS") \
208
+ <(echo "$CAND_TOKENS") \
209
+ | grep -c . || true)
210
+
211
+ # Shorter token set size (for percentage threshold)
212
+ if [[ "$PROPOSED_TOKEN_COUNT" -le "$CAND_TOKEN_COUNT" ]]; then
213
+ SHORTER=$PROPOSED_TOKEN_COUNT
214
+ else
215
+ SHORTER=$CAND_TOKEN_COUNT
216
+ fi
217
+
218
+ # 50% threshold: shared >= ceil(shorter / 2)
219
+ HALF_THRESHOLD=$(( (SHORTER + 1) / 2 ))
220
+
221
+ if [[ "$SHARED_COUNT" -ge 3 ]] || [[ "$SHARED_COUNT" -ge "$HALF_THRESHOLD" && "$SHORTER" -ge 2 ]]; then
222
+ MATCH_NUMBER="$CAND_NUMBER"
223
+ MATCH_TITLE="$CAND_TITLE"
224
+ break
225
+ fi
226
+ done <<< "$CANDIDATES"
227
+
228
+ # --------------------------------------------------------------------------- #
229
+ # Output result
230
+ # --------------------------------------------------------------------------- #
231
+ if [[ -n "$MATCH_NUMBER" ]]; then
232
+ echo "DUPLICATE: #${MATCH_NUMBER} ${MATCH_TITLE}"
233
+ exit 1
234
+ fi
235
+
236
+ exit 0
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ // SPDX-License-Identifier: MIT
4
+ //
5
+ // validate-annotation-node.mjs — Thin Node.js adapter: validates FORGE annotation bodies
6
+ // against the MIT/Apache protocol validation library (packages/protocol/, built in #1291).
7
+ //
8
+ // This script is the dogfooding consumer for the protocol library (#1291), satisfying
9
+ // the requirement from #1292. It wraps the library's validate() API with:
10
+ // - Graceful degradation when the library is not yet installed (exits 0 with WARN)
11
+ // - A stable CLI that matches forge-annotation.sh validate's argument shape,
12
+ // so future forge-annotation.sh validate integration (#1267) is a one-line shim
13
+ // - MIT license: this file is NOT AGPL — it must remain importable by MIT/Apache
14
+ // consumers without license contamination. Do not import AGPL modules here.
15
+ //
16
+ // Usage:
17
+ // node scripts/validate-annotation-node.mjs <MARKER> [FILE]
18
+ // echo '<!-- FORGE:INVESTIGATOR -->...' | node scripts/validate-annotation-node.mjs INVESTIGATOR
19
+ //
20
+ // Arguments:
21
+ // MARKER Annotation type to validate (e.g. INVESTIGATOR, CONTRACT, BUILDER).
22
+ // May include the FORGE: prefix — it will be stripped automatically.
23
+ // FILE Path to a file containing the annotation body. If omitted (or '-'),
24
+ // reads from stdin.
25
+ //
26
+ // Exit codes:
27
+ // 0 = annotation is well-formed, OR library is not yet installed (graceful degradation)
28
+ // 1 = annotation is malformed (library reports a conformance error)
29
+ // 2 = usage/argument error (bad MARKER, unreadable FILE, etc.)
30
+ //
31
+ // Integration notes:
32
+ // - When packages/protocol/ (issue #1291) is built and published, this script will
33
+ // work without any changes. The dynamic import path resolves relative to this script.
34
+ // - When forge-annotation.sh (issue #1267) is merged, its `validate` subcommand can
35
+ // shell out to this script for conformance checks: the Bash side handles sentinel
36
+ // and structure (AGPL, format rules), the Node side handles spec conformance (MIT).
37
+ // - Call-site wiring in commands/*.md is tracked separately in #1247.
38
+
39
+ import { readFileSync, existsSync } from 'node:fs';
40
+ import { createRequire } from 'node:module';
41
+ import { resolve, dirname } from 'node:path';
42
+ import { fileURLToPath, pathToFileURL } from 'node:url';
43
+
44
+ const SCRIPT_NAME = 'validate-annotation-node.mjs';
45
+ const __filename = fileURLToPath(import.meta.url);
46
+ const __dirname = dirname(__filename);
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Usage
50
+ // ---------------------------------------------------------------------------
51
+ function usage() {
52
+ process.stderr.write(
53
+ `Usage:\n` +
54
+ ` ${SCRIPT_NAME} <MARKER> [FILE]\n` +
55
+ ` echo '<body>' | ${SCRIPT_NAME} <MARKER>\n` +
56
+ `\n` +
57
+ `Arguments:\n` +
58
+ ` MARKER Annotation type (e.g. INVESTIGATOR, FORGE:INVESTIGATOR, CONTRACT, BUILDER)\n` +
59
+ ` FILE File containing the annotation body (reads stdin if omitted or '-')\n` +
60
+ `\n` +
61
+ `Exit codes:\n` +
62
+ ` 0 Valid annotation, or library not yet installed (graceful degradation)\n` +
63
+ ` 1 Invalid annotation (library conformance check failed)\n` +
64
+ ` 2 Usage/argument error\n`
65
+ );
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Normalize marker: strip FORGE: prefix, uppercase
70
+ // Mirrors forge-annotation.sh normalize_marker() so both tools agree on marker names.
71
+ // ---------------------------------------------------------------------------
72
+ function normalizeMarker(raw) {
73
+ return raw.toUpperCase().replace(/^FORGE:/, '');
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Read body from file path or stdin
78
+ // ---------------------------------------------------------------------------
79
+ async function readBody(fileArg) {
80
+ if (!fileArg || fileArg === '-') {
81
+ // Read from stdin
82
+ return new Promise((resolve, reject) => {
83
+ const chunks = [];
84
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
85
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
86
+ process.stdin.on('error', reject);
87
+ });
88
+ }
89
+
90
+ if (!existsSync(fileArg)) {
91
+ process.stderr.write(`ERROR: file not found: ${fileArg}\n`);
92
+ process.exit(2);
93
+ }
94
+
95
+ try {
96
+ return readFileSync(fileArg, 'utf8');
97
+ } catch (err) {
98
+ process.stderr.write(`ERROR: cannot read file '${fileArg}': ${err.message}\n`);
99
+ process.exit(2);
100
+ }
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Locate the protocol library
105
+ // Library expected at packages/protocol/src/index.js relative to the repo
106
+ // root (per packages/protocol/package.json main/exports).
107
+ // Repo root is two directories above this script (scripts/ → repo root).
108
+ // ---------------------------------------------------------------------------
109
+ function resolveLibraryPath() {
110
+ const repoRoot = resolve(__dirname, '..');
111
+ return resolve(repoRoot, 'packages', 'protocol', 'src', 'index.js');
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Main
116
+ // ---------------------------------------------------------------------------
117
+ async function main() {
118
+ const args = process.argv.slice(2);
119
+
120
+ if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
121
+ usage();
122
+ process.exit(args.length === 0 ? 2 : 0);
123
+ }
124
+
125
+ const markerRaw = args[0];
126
+ const fileArg = args[1] || null;
127
+ const marker = normalizeMarker(markerRaw);
128
+
129
+ if (!marker || !/^[A-Z_]+$/.test(marker)) {
130
+ process.stderr.write(`ERROR: invalid MARKER '${markerRaw}' — must be a non-empty string (letters and underscores only after prefix stripping)\n`);
131
+ usage();
132
+ process.exit(2);
133
+ }
134
+
135
+ const body = await readBody(fileArg);
136
+
137
+ if (!body.trim()) {
138
+ process.stderr.write(`ERROR: annotation body is empty — nothing to validate\n`);
139
+ process.exit(2);
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Attempt to load the protocol library (graceful degradation if absent)
144
+ // ---------------------------------------------------------------------------
145
+ const libraryPath = resolveLibraryPath();
146
+
147
+ if (!existsSync(libraryPath)) {
148
+ // Library not yet built (#1291 not merged) — exit 0 with a warning.
149
+ // This is the expected state until #1291 lands; the pipeline must not break.
150
+ process.stdout.write(
151
+ `WARN: forge-protocol library not found at packages/protocol/src/index.js\n` +
152
+ ` Conformance validation skipped — install the library from #1291 to enable spec-conformance checks.\n` +
153
+ ` Annotation posting will proceed without library validation.\n`
154
+ );
155
+ process.exit(0);
156
+ }
157
+
158
+ let library;
159
+ try {
160
+ // Dynamic import() requires a file:// URL for absolute paths on Windows
161
+ // (a bare "C:\..." path is misparsed as a URL with protocol "c:"). Using
162
+ // pathToFileURL() makes this work identically on Windows and POSIX.
163
+ library = await import(pathToFileURL(libraryPath));
164
+ } catch (err) {
165
+ // Library file exists but failed to load — log and degrade gracefully.
166
+ process.stdout.write(
167
+ `WARN: forge-protocol library found at packages/protocol/src/index.js but failed to load: ${err.message}\n` +
168
+ ` Conformance validation skipped. Check that the library is built correctly.\n`
169
+ );
170
+ process.exit(0);
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Parse the body into a structured annotation, then call library.validate().
175
+ // The library's validate() expects a ParsedAnnotation (the output of parse()),
176
+ // NOT a raw { type, body } literal — validate() reads annotation.isReserved,
177
+ // .fields, .sentinelState, etc., all of which only exist after parse() runs.
178
+ // Passing an unparsed literal makes validate() take its "unknown type, tolerate"
179
+ // early-return path (§7.2.4) and always report { valid: true }, silently
180
+ // skipping every real conformance check.
181
+ // ---------------------------------------------------------------------------
182
+ if (typeof library.parse !== 'function' || typeof library.validate !== 'function') {
183
+ process.stderr.write(
184
+ `ERROR: forge-protocol library does not export the expected 'parse' and 'validate' functions.\n` +
185
+ ` Found exports: ${Object.keys(library).join(', ') || '(none)'}\n`
186
+ );
187
+ process.exit(2);
188
+ }
189
+
190
+ let annotation;
191
+ try {
192
+ const parsed = library.parse(body);
193
+ annotation = parsed.find((a) => a.type === marker);
194
+ } catch (err) {
195
+ process.stderr.write(`ERROR: library.parse() threw an exception: ${err.message}\n`);
196
+ process.exit(1);
197
+ }
198
+
199
+ if (!annotation) {
200
+ process.stderr.write(
201
+ `ERROR: no '${marker}' annotation found in the provided body — expected an opening tag ` +
202
+ `like <!-- FORGE:${marker} --> somewhere in the input.\n`
203
+ );
204
+ process.exit(2);
205
+ }
206
+
207
+ let result;
208
+ try {
209
+ result = library.validate(annotation);
210
+ } catch (err) {
211
+ process.stderr.write(`ERROR: library.validate() threw an exception: ${err.message}\n`);
212
+ process.exit(1);
213
+ }
214
+
215
+ if (result && result.valid === true) {
216
+ process.stdout.write(`OK: '${marker}' annotation is well-formed (forge-protocol conformance check passed)\n`);
217
+ process.exit(0);
218
+ } else {
219
+ const errors = (result && Array.isArray(result.errors) && result.errors.length > 0)
220
+ ? result.errors.join('\n - ')
221
+ : 'no error details returned by library';
222
+ process.stderr.write(
223
+ `ERROR: '${marker}' annotation failed forge-protocol conformance check:\n` +
224
+ ` - ${errors}\n`
225
+ );
226
+ process.exit(1);
227
+ }
228
+ }
229
+
230
+ main().catch((err) => {
231
+ process.stderr.write(`FATAL: ${err.message}\n`);
232
+ process.exit(2);
233
+ });
@@ -179,8 +179,24 @@ for var in $VAR_NAMES; do
179
179
  fi
180
180
 
181
181
  if [ -z "$FOUND_IN" ]; then
182
- echo "BLOCKING: $var — not found in .env.example, docker-compose.yml, or env_validation.py"
183
- BLOCKING=$((BLOCKING + 1))
182
+ # Determine whether any of the optional surfaces exist in this repo.
183
+ # If NONE of the known surfaces exist (not even .env.example), the repo
184
+ # uses a layout we can't validate — emit SKIP instead of a false BLOCKING.
185
+ # This is the verify-sops-chain.sh pattern: unknown layout → graceful skip,
186
+ # not a spurious failure. <!-- Added: forge#1349 -->
187
+ HAS_ANY_SURFACE=0
188
+ [ -f "$REPO_ROOT/.env.example" ] && HAS_ANY_SURFACE=1
189
+ [ -f "$REPO_ROOT/docker-compose.yml" ] && HAS_ANY_SURFACE=1
190
+ [ -f "$REPO_ROOT/docker-compose.prod.yml" ] && HAS_ANY_SURFACE=1
191
+ [ -f "$REPO_ROOT/services/api/app/core/env_validation.py" ] && HAS_ANY_SURFACE=1
192
+ [ -f "$REPO_ROOT/scripts/decrypt-secrets.sh" ] && HAS_ANY_SURFACE=1
193
+
194
+ if [ "$HAS_ANY_SURFACE" -eq 0 ]; then
195
+ echo "SKIP: $var — no known env-wiring surfaces found in repo (no .env.example, docker-compose, env_validation.py, or SOPS mapping). Configure verification.env_check_surfaces in forge.yaml or add .env.example to enable this check."
196
+ else
197
+ echo "BLOCKING: $var — not found in any detected env-wiring surface (.env.example, docker-compose.yml, env_validation.py, or decrypt-secrets.sh)"
198
+ BLOCKING=$((BLOCKING + 1))
199
+ fi
184
200
  else
185
201
  echo "OK: $var — found in:$FOUND_IN"
186
202
  fi
@@ -67,9 +67,9 @@ while read -r f; do
67
67
  elif echo "$CONTENT" | grep -qE "\-\-header\s+[\"']Host:"; then
68
68
  echo "OK: $f:$LINENO — Host header explicitly set (--header)"
69
69
  else
70
- echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. TrustedHostMiddleware will reject raw IP as Host value."
70
+ echo "BLOCKING: $f:$LINENO — curl/wget to internal service without explicit Host header. Host-validation middleware will reject a raw IP as the Host value."
71
71
  echo " Line: $CONTENT"
72
- echo " Fix: Add -H 'Host: localhost' (or appropriate allowed host from services/api/app/main.py allowed_hosts)"
72
+ echo " Fix: Add -H 'Host: <allowed-hostname>' — check your service's allowed_hosts configuration for the correct value"
73
73
  BLOCKING=$((BLOCKING + 1))
74
74
  fi
75
75
  done < <(echo "$CURL_LINES")
@@ -96,11 +96,11 @@ if [ -n "$FRONTEND_FILES" ]; then
96
96
  fi
97
97
 
98
98
  if [ -n "$DIRECT_CALLS" ]; then
99
- echo "BLOCKING: $f — direct /api/v1/ call bypasses Next.js proxy (auth will fail)"
99
+ echo "BLOCKING: $f — direct /api/v1/ call bypasses frontend proxy (auth middleware will be skipped)"
100
100
  while IFS= read -r match; do
101
101
  echo " $match"
102
102
  done < <(echo "$DIRECT_CALLS")
103
- echo " Fix: Use /api/* proxy routes instead of /api/v1/* direct FastAPI calls"
103
+ echo " Fix: Use the frontend proxy route (e.g. /api/*) instead of calling the backend API directly"
104
104
  BLOCKING=$((BLOCKING + 1))
105
105
  else
106
106
  echo "OK: $f — no direct backend calls"
@@ -5,7 +5,7 @@
5
5
  # changed_files_file: path to a file listing changed files (one per line)
6
6
  # repo_root: path to the repository root
7
7
  #
8
- # Layout path overrides (env vars — override defaults for non-AlterLab layouts):
8
+ # Layout path overrides (env vars — override defaults for project-specific layouts):
9
9
  # FORGE_PAGES_ROOT Root of Next.js App Router pages dir (default: web/src/app)
10
10
  # FORGE_API_ROUTERS_DIR Dir containing FastAPI router .py files (default: services/api/app/routers)
11
11
  # FORGE_API_MAIN Path to API app entrypoint, relative to repo root (default: services/api/app/main.py)
@@ -35,7 +35,7 @@ else
35
35
  FILES=$(cat "$CHANGED_FILES")
36
36
  fi
37
37
 
38
- # Layout path configuration — read from env vars, fall back to AlterLab defaults
38
+ # Layout path configuration — read from env vars, fall back to project defaults
39
39
  FORGE_PAGES_ROOT="${FORGE_PAGES_ROOT:-web/src/app}"
40
40
  FORGE_API_ROUTERS_DIR="${FORGE_API_ROUTERS_DIR:-services/api/app/routers}"
41
41
  FORGE_API_MAIN="${FORGE_API_MAIN:-services/api/app/main.py}"