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.
- package/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- 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
|
-
|
|
183
|
-
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}"
|