forgedock 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/bin/engine/phases.mjs +178 -28
- package/bin/engine/state.mjs +39 -7
- package/bin/engine-cli.mjs +63 -2
- package/bin/engine.mjs +25 -4
- package/bin/forgedock.mjs +235 -4
- package/bin/hooks/pre-tool-use.mjs +26 -12
- package/bin/journey.mjs +103 -11
- package/commands/forge-stats.md +5 -4
- package/commands/issue.md +5 -1
- package/commands/orchestrate/phase-4-execution.md +2 -0
- package/commands/qa-sweep.md +17 -0
- package/commands/replay.md +16 -2
- package/commands/review-pr-agents/spec-cli.md +179 -0
- package/commands/review-pr.md +49 -0
- package/commands/upgrade-deps.md +518 -0
- package/commands/work-on/build/validate.md +61 -1
- package/commands/work-on/build.md +9 -1
- package/commands/work-on/decompose.md +8 -1
- package/docs/AIDER.md +264 -0
- package/docs/AUTOPILOT-CRON.md +130 -0
- package/docs/CONFIG.md +36 -0
- package/docs/CURSOR.md +135 -0
- package/docs/OPENCODE.md +312 -0
- package/docs/demo.gif +0 -0
- package/docs/eval/README.md +4 -4
- package/docs/launch/show-hn.md +3 -3
- package/docs/site/architecture.md +180 -0
- package/docs/site/command-reference.md +17 -0
- package/docs/site/configuration.md +243 -0
- package/docs/site/for-companies.md +4 -4
- package/docs/site/forge-annotation-protocol.md +1 -1
- package/docs/site/github-is-the-memory.md +2 -2
- package/docs/site/how-it-works.md +1 -1
- package/docs/site/public/llms-full.txt +380 -0
- package/docs/site/public/llms.txt +35 -0
- package/docs/spec-graph-schema.md +3 -3
- package/package.json +2 -2
- package/packages/protocol/README.md +75 -0
- package/packages/protocol/src/card.js +114 -0
- package/packages/protocol/src/cli.js +4 -77
- package/packages/protocol/src/emit.js +14 -3
- package/packages/protocol/src/index.js +2 -1
- package/packages/protocol/src/parse.js +17 -8
- package/packages/protocol/src/types.js +1 -1
- package/packages/protocol/src/validate.js +11 -0
- package/scripts/check-command-side-effects.sh +261 -0
- package/scripts/check-spec-markers.sh +273 -0
- package/scripts/classify-lane.sh +29 -7
- package/scripts/forge-run.sh +322 -0
- package/templates/cross-repo-trigger.yml +282 -0
- package/templates/cursor/forge.mdc +148 -0
- package/templates/github-actions/forgedock-autopilot.yml +113 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
|
|
3
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# check-spec-markers.sh — Validate FORGE: annotation markers in commands/**/*.md and
|
|
6
|
+
# detect unsubstituted {PLACEHOLDER} tokens in CI workflow files.
|
|
7
|
+
#
|
|
8
|
+
# Command specs contain two classes of spec-as-code marker defects:
|
|
9
|
+
#
|
|
10
|
+
# 1. Non-registry FORGE: markers — a typo or invented marker (e.g. FORGE:INVESTIAGOR,
|
|
11
|
+
# FORGE:NOTES) in a command spec will cause agents to look for a comment annotation
|
|
12
|
+
# that is never emitted, silently breaking pipeline state detection. (Ref: forge#633)
|
|
13
|
+
#
|
|
14
|
+
# 2. Unsubstituted {PLACEHOLDER} tokens in CI workflow YAML — a template variable like
|
|
15
|
+
# {GH_REPO} or {NUMBER} left unsubstituted in a workflow inline script means every
|
|
16
|
+
# CI run executes a malformed command that fails silently or produces wrong output.
|
|
17
|
+
# (Ref: forge#318; note: {PLACEHOLDER} tokens in commands/*.md are intentional
|
|
18
|
+
# spec notation and are NOT flagged by this check.)
|
|
19
|
+
#
|
|
20
|
+
# This script:
|
|
21
|
+
# 1. Scans all *.md files in commands/ for <!-- FORGE:XXXX --> annotations.
|
|
22
|
+
# 2. Validates XXXX against the known registry (see MARKER_REGISTRY below).
|
|
23
|
+
# 3. Scans .github/workflows/*.yml for unsubstituted {PLACEHOLDER} tokens in run: blocks.
|
|
24
|
+
#
|
|
25
|
+
# Registry: covers all reserved types from packages/protocol/src/types.js
|
|
26
|
+
# (RESERVED_TYPE_NAMES) plus all operational/pipeline markers used throughout the
|
|
27
|
+
# ForgeDock spec corpus. Compound markers (FORGE:TYPE-SUBTYPE, FORGE:TYPE:SUBTYPE)
|
|
28
|
+
# are validated by their primary type (the part before the first - or :).
|
|
29
|
+
# Update the registry when adding new marker types.
|
|
30
|
+
#
|
|
31
|
+
# Allowlist: add <!-- allowlist:check-spec-markers --> on the same line as the marker
|
|
32
|
+
# to suppress that specific hit.
|
|
33
|
+
#
|
|
34
|
+
# Usage:
|
|
35
|
+
# check-spec-markers.sh [<commands_dir> [<repo_root>]]
|
|
36
|
+
# commands_dir: path to the commands/ directory (default: ./commands)
|
|
37
|
+
# repo_root: path to repo root for workflow scanning (default: .)
|
|
38
|
+
#
|
|
39
|
+
# Exit codes:
|
|
40
|
+
# 0 no violations found
|
|
41
|
+
# 1 one or more violations found (listed to stderr)
|
|
42
|
+
# 2 usage / dependency error (commands_dir not found)
|
|
43
|
+
#
|
|
44
|
+
# <!-- Added: forge#1609 -->
|
|
45
|
+
|
|
46
|
+
set -euo pipefail
|
|
47
|
+
|
|
48
|
+
COMMANDS_DIR="${1:-./commands}"
|
|
49
|
+
REPO_ROOT="${2:-.}"
|
|
50
|
+
|
|
51
|
+
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
|
52
|
+
sed -n '2,/^set -/p' "$0" | grep '^#' | sed 's/^# *//'
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if [ ! -d "$COMMANDS_DIR" ]; then
|
|
57
|
+
echo "ERROR: commands directory not found: $COMMANDS_DIR" >&2
|
|
58
|
+
echo "Usage: $0 [<commands_dir> [<repo_root>]]" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# FORGE: marker registry
|
|
64
|
+
#
|
|
65
|
+
# Sources:
|
|
66
|
+
# - packages/protocol/src/types.js — RESERVED_TYPE_NAMES (reserved lifecycle types)
|
|
67
|
+
# - Operational pipeline markers used in commands/**/*.md specs
|
|
68
|
+
#
|
|
69
|
+
# Compound markers (FORGE:REVIEW-AGENT:material-change, FORGE:PHASE:COMPLETE) are
|
|
70
|
+
# validated by their PRIMARY type only (the part before the first - or : after FORGE:).
|
|
71
|
+
# Add entries for each primary type; sub-types are unrestricted.
|
|
72
|
+
#
|
|
73
|
+
# Update this list when new marker types are added to the protocol or spec corpus.
|
|
74
|
+
# Keep sorted alphabetically for readability.
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
MARKER_REGISTRY="
|
|
77
|
+
ACCEPTANCE_GATE
|
|
78
|
+
ADR_EXTRACTED
|
|
79
|
+
ANCESTRY_FAILED
|
|
80
|
+
ARCHITECT
|
|
81
|
+
AUDIT
|
|
82
|
+
AUTOPILOT_CYCLE
|
|
83
|
+
AUTOPILOT_GATE
|
|
84
|
+
AUTOPILOT_IMPACT
|
|
85
|
+
BATCHABLE
|
|
86
|
+
BATCH_ID
|
|
87
|
+
BATCH_MEMBERS
|
|
88
|
+
BENCH_SCORECARD
|
|
89
|
+
BLOCKED_ON_HUMAN_MERGE
|
|
90
|
+
BUILDER
|
|
91
|
+
CALIBRATION_CHECK
|
|
92
|
+
CARD
|
|
93
|
+
CHECKPOINT
|
|
94
|
+
CLAIM
|
|
95
|
+
CLAIM_RELEASED
|
|
96
|
+
CONTEXT
|
|
97
|
+
CONTRACT
|
|
98
|
+
COORD_ISSUE
|
|
99
|
+
CRITIQUE
|
|
100
|
+
DECISION_RECORD
|
|
101
|
+
DECOMPOSED
|
|
102
|
+
DESIGN
|
|
103
|
+
DISPATCHER
|
|
104
|
+
DOSSIER_UPDATED
|
|
105
|
+
FAST_PATH
|
|
106
|
+
FINDING_SOURCE
|
|
107
|
+
FIX_CI
|
|
108
|
+
GATE_FAILED
|
|
109
|
+
GATE_FAILURE
|
|
110
|
+
GATE_PASS
|
|
111
|
+
HEARTBEAT
|
|
112
|
+
INDEP_VERIFY
|
|
113
|
+
INDEP_VERIFY_FAIL
|
|
114
|
+
INDEP_VERIFY_PASS
|
|
115
|
+
INVESTIGATOR
|
|
116
|
+
JSONL_PARSER_UTIL
|
|
117
|
+
KNOWLEDGE_GIST
|
|
118
|
+
LEARNED
|
|
119
|
+
LEARNED_RULES
|
|
120
|
+
LEDGER_INDEXED
|
|
121
|
+
MEMORY_INDEX
|
|
122
|
+
MEMORY_INDEXED
|
|
123
|
+
MILESTONE_INDEX
|
|
124
|
+
MODEL_TIER_NOTE
|
|
125
|
+
ORPHAN_ESCALATED
|
|
126
|
+
ORPHAN_RECOVERED
|
|
127
|
+
PATTERN
|
|
128
|
+
PHASE
|
|
129
|
+
PHASE_COMPLETE
|
|
130
|
+
PLAN_DAG
|
|
131
|
+
PRIOR_DECISIONS
|
|
132
|
+
PRIOR_GIST
|
|
133
|
+
PROTOCOL_SOURCE
|
|
134
|
+
PUSH_BLOCKED
|
|
135
|
+
PUSH_BLOCKED_EMPTY_BRANCH
|
|
136
|
+
PUSH_FAILED
|
|
137
|
+
REMEDIATION
|
|
138
|
+
REVIEW
|
|
139
|
+
REVIEWER
|
|
140
|
+
REVIEW_ROUTE
|
|
141
|
+
REVIEW_STARTED
|
|
142
|
+
SECURITY_AUDIT
|
|
143
|
+
SIGNAL_RESOLVED
|
|
144
|
+
SIGNAL_UNRESOLVED
|
|
145
|
+
SPAWN_POLICY
|
|
146
|
+
SPEC_DOCTOR_COMPLETE
|
|
147
|
+
SPEC_LOADED
|
|
148
|
+
STALL_DETECTED
|
|
149
|
+
STATE
|
|
150
|
+
SYNTHESIS_BRIEF
|
|
151
|
+
TEST_GATE
|
|
152
|
+
TRAIN_CANDIDATE
|
|
153
|
+
TRAJECTORY
|
|
154
|
+
UNBLOCKED
|
|
155
|
+
USER_FEEDBACK
|
|
156
|
+
"
|
|
157
|
+
|
|
158
|
+
# Build an alternation pattern for registry lookup.
|
|
159
|
+
# Words separated by | for use with grep -E: ^(WORD1|WORD2|...)$
|
|
160
|
+
REGISTRY_NAMES=$(echo "$MARKER_REGISTRY" | tr -s '[:space:]' '\n' | grep -vE '^\s*$' | sort -u | tr '\n' '|' | sed 's/|$//')
|
|
161
|
+
REGISTRY_PATTERN="^(${REGISTRY_NAMES})$"
|
|
162
|
+
|
|
163
|
+
# Allowlist token — a line containing this token is exempt
|
|
164
|
+
ALLOWLIST_TOKEN='allowlist:check-spec-markers'
|
|
165
|
+
|
|
166
|
+
VIOLATIONS=0
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Pass 1: Validate FORGE: markers in commands/**/*.md
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
find_md_files() {
|
|
173
|
+
find "$COMMANDS_DIR" -name '*.md' | sort
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while IFS= read -r file; do
|
|
177
|
+
[ -f "$file" ] || continue
|
|
178
|
+
|
|
179
|
+
# Extract all lines containing <!-- FORGE:XXXX --> annotations with their line numbers.
|
|
180
|
+
while IFS= read -r markerline; do
|
|
181
|
+
[ -z "$markerline" ] && continue
|
|
182
|
+
lineno="${markerline%%:*}"
|
|
183
|
+
content="${markerline#*:}"
|
|
184
|
+
|
|
185
|
+
# Skip allowlisted lines
|
|
186
|
+
if echo "$content" | grep -qF "$ALLOWLIST_TOKEN"; then
|
|
187
|
+
continue
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# Skip markers that are inside code spans (backtick-quoted) — those are docs examples
|
|
191
|
+
# Heuristic: if the FORGE:TYPE appears between backticks on the same line, skip it
|
|
192
|
+
# We check for backtick on either side of the FORGE: marker
|
|
193
|
+
if echo "$content" | grep -qE "\`[^\`]*FORGE:[A-Z_]"; then
|
|
194
|
+
continue
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# Extract the full marker string after FORGE: — everything up to a space, -->, or end
|
|
198
|
+
# This captures compound markers like REVIEW-AGENT:material-change
|
|
199
|
+
full_marker=$(echo "$content" | grep -oE 'FORGE:[A-Z][A-Z0-9_:-]*' | head -1 | sed 's/^FORGE://')
|
|
200
|
+
|
|
201
|
+
[ -z "$full_marker" ] && continue
|
|
202
|
+
|
|
203
|
+
# Extract PRIMARY type: part before the first - or : separator
|
|
204
|
+
primary_type=$(echo "$full_marker" | sed 's/[-:].*//')
|
|
205
|
+
|
|
206
|
+
# Validate primary type against registry
|
|
207
|
+
if ! echo "$primary_type" | grep -qE "$REGISTRY_PATTERN"; then
|
|
208
|
+
echo "HIGH | $file:$lineno | unknown FORGE: marker 'FORGE:${full_marker}' (primary type '${primary_type}') — not in registry" >&2
|
|
209
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
210
|
+
fi
|
|
211
|
+
done < <(grep -nE '<!--[[:space:]]*FORGE:[A-Z]' "$file" 2>/dev/null || true)
|
|
212
|
+
|
|
213
|
+
done < <(find_md_files)
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Pass 2: Detect unsubstituted {PLACEHOLDER} in .github/workflows/*.yml
|
|
217
|
+
#
|
|
218
|
+
# In workflow YAML, {PLACEHOLDER} tokens (not bash ${VAR} expansions) indicate
|
|
219
|
+
# a template variable that should have been resolved before committing.
|
|
220
|
+
#
|
|
221
|
+
# Bash ${VAR} references are excluded — we strip ${...} patterns before scanning.
|
|
222
|
+
# Pattern: {UPPERCASE_3+} — e.g. {GH_REPO}, {NUMBER}, {WORKTREE_PATH}
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
WORKFLOWS_DIR="${REPO_ROOT}/.github/workflows"
|
|
226
|
+
|
|
227
|
+
if [ -d "$WORKFLOWS_DIR" ]; then
|
|
228
|
+
# Pattern: { followed by uppercase letter, then 2+ uppercase/digit/underscore, then }
|
|
229
|
+
# Preceded by something other than $ (to exclude bash ${VAR} expansions)
|
|
230
|
+
PLACEHOLDER_PATTERN='\{[A-Z][A-Z0-9_]{2,}\}'
|
|
231
|
+
|
|
232
|
+
while IFS= read -r wf_file; do
|
|
233
|
+
[ -f "$wf_file" ] || continue
|
|
234
|
+
|
|
235
|
+
LINENO=0
|
|
236
|
+
|
|
237
|
+
while IFS= read -r line; do
|
|
238
|
+
LINENO=$((LINENO + 1))
|
|
239
|
+
|
|
240
|
+
# Skip allowlisted lines
|
|
241
|
+
if echo "$line" | grep -qF "$ALLOWLIST_TOKEN"; then
|
|
242
|
+
continue
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# Skip comment lines
|
|
246
|
+
if echo "$line" | grep -qE '^[[:space:]]*#'; then
|
|
247
|
+
continue
|
|
248
|
+
fi
|
|
249
|
+
|
|
250
|
+
# Strip bash ${VAR} and $VAR patterns before checking for template placeholders
|
|
251
|
+
# so that ELAPSED in "${ELAPSED}s" doesn't false-positive as {ELAPSED}
|
|
252
|
+
stripped=$(echo "$line" | sed 's/\${\([^}]*\)}/BASH_VAR/g; s/\$[A-Za-z_][A-Za-z0-9_]*/BASH_VAR/g')
|
|
253
|
+
|
|
254
|
+
if echo "$stripped" | grep -qE "$PLACEHOLDER_PATTERN"; then
|
|
255
|
+
placeholder=$(echo "$stripped" | grep -oE "$PLACEHOLDER_PATTERN" | head -1)
|
|
256
|
+
echo "HIGH | $wf_file:$LINENO | unsubstituted placeholder '$placeholder' in workflow" >&2
|
|
257
|
+
VIOLATIONS=$((VIOLATIONS + 1))
|
|
258
|
+
fi
|
|
259
|
+
done < "$wf_file"
|
|
260
|
+
done < <(find "$WORKFLOWS_DIR" -name '*.yml' | sort)
|
|
261
|
+
fi
|
|
262
|
+
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
# Summary
|
|
265
|
+
# ---------------------------------------------------------------------------
|
|
266
|
+
|
|
267
|
+
if [ "$VIOLATIONS" -gt 0 ]; then
|
|
268
|
+
echo "check-spec-markers: $VIOLATIONS violation(s) found. See stderr for details." >&2
|
|
269
|
+
exit 1
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
echo "OK: No marker or placeholder violations found"
|
|
273
|
+
exit 0
|
package/scripts/classify-lane.sh
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# classify-lane.sh — Deterministic lane routing from issue milestone
|
|
3
3
|
#
|
|
4
|
-
# Usage: classify-lane.sh <issue_number> [-R <owner/repo>]
|
|
4
|
+
# Usage: classify-lane.sh <issue_number> [-R <owner/repo>] [--json]
|
|
5
5
|
# issue_number: GitHub issue number (required)
|
|
6
6
|
# -R <owner/repo>: GitHub repository (optional, defaults to current repo)
|
|
7
|
+
# --json: emit structured JSON object instead of plain lane string (optional)
|
|
7
8
|
#
|
|
8
|
-
# Output: lane string written to stdout
|
|
9
|
+
# Output: lane string written to stdout (default)
|
|
9
10
|
# staging — issue has no milestone (fast lane)
|
|
10
11
|
# milestone/{slug} — issue has a milestone (feature lane, slug = lowercased, spaces→hyphens, git-invalid chars stripped)
|
|
11
12
|
#
|
|
13
|
+
# Output: JSON object when --json flag is passed
|
|
14
|
+
# {"lane":"staging","branch":"staging","source":"fast-lane","milestone":null}
|
|
15
|
+
# {"lane":"milestone/{slug}","branch":"milestone/{slug}","source":"feature-lane","milestone":"{title}","slug":"{slug}"}
|
|
16
|
+
#
|
|
12
17
|
# Exit codes: 0 = success, 1 = error (invalid issue, gh auth failure, branch missing, etc.)
|
|
13
18
|
#
|
|
14
19
|
# Branch existence validation:
|
|
@@ -23,11 +28,12 @@ set -euo pipefail
|
|
|
23
28
|
|
|
24
29
|
ISSUE_NUMBER="${1:-}"
|
|
25
30
|
GH_REPO_ARGS=()
|
|
31
|
+
JSON_OUTPUT=false
|
|
26
32
|
|
|
27
|
-
# Parse arguments: issue number + optional -R flag
|
|
33
|
+
# Parse arguments: issue number + optional -R flag + optional --json flag
|
|
28
34
|
if [ -z "$ISSUE_NUMBER" ]; then
|
|
29
35
|
echo "ERROR: issue number is required" >&2
|
|
30
|
-
echo "Usage: classify-lane.sh <issue_number> [-R <owner/repo>]" >&2
|
|
36
|
+
echo "Usage: classify-lane.sh <issue_number> [-R <owner/repo>] [--json]" >&2
|
|
31
37
|
exit 1
|
|
32
38
|
fi
|
|
33
39
|
|
|
@@ -37,7 +43,7 @@ while [ $# -gt 0 ]; do
|
|
|
37
43
|
-R)
|
|
38
44
|
if [ $# -lt 2 ]; then
|
|
39
45
|
echo "ERROR: -R requires a value <owner/repo>" >&2
|
|
40
|
-
echo "Usage: classify-lane.sh <issue_number> [-R <owner/repo>]" >&2
|
|
46
|
+
echo "Usage: classify-lane.sh <issue_number> [-R <owner/repo>] [--json]" >&2
|
|
41
47
|
exit 1
|
|
42
48
|
fi
|
|
43
49
|
if ! [[ "$2" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
|
@@ -47,6 +53,10 @@ while [ $# -gt 0 ]; do
|
|
|
47
53
|
GH_REPO_ARGS=(-R "$2")
|
|
48
54
|
shift 2
|
|
49
55
|
;;
|
|
56
|
+
--json)
|
|
57
|
+
JSON_OUTPUT=true
|
|
58
|
+
shift
|
|
59
|
+
;;
|
|
50
60
|
*)
|
|
51
61
|
echo "ERROR: unknown argument: $1" >&2
|
|
52
62
|
exit 1
|
|
@@ -94,7 +104,12 @@ if [ -z "$MILESTONE_TITLE" ]; then
|
|
|
94
104
|
echo " Run: git push origin HEAD:$STAGING_BRANCH (from the default branch)" >&2
|
|
95
105
|
exit 1
|
|
96
106
|
fi
|
|
97
|
-
|
|
107
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
108
|
+
printf '{"lane":"%s","branch":"%s","source":"fast-lane","milestone":null}\n' \
|
|
109
|
+
"$STAGING_BRANCH" "$STAGING_BRANCH"
|
|
110
|
+
else
|
|
111
|
+
echo "$STAGING_BRANCH"
|
|
112
|
+
fi
|
|
98
113
|
else
|
|
99
114
|
# Slugify: lowercase, spaces → hyphens, strip git-invalid chars, collapse multiple hyphens,
|
|
100
115
|
# strip leading/trailing hyphens.
|
|
@@ -134,5 +149,12 @@ else
|
|
|
134
149
|
exit 1
|
|
135
150
|
fi
|
|
136
151
|
|
|
137
|
-
|
|
152
|
+
if [ "$JSON_OUTPUT" = "true" ]; then
|
|
153
|
+
# Escape double quotes in milestone title for safe JSON embedding.
|
|
154
|
+
MILESTONE_ESCAPED="${MILESTONE_TITLE//\"/\\\"}"
|
|
155
|
+
printf '{"lane":"%s","branch":"%s","source":"feature-lane","milestone":"%s","slug":"%s"}\n' \
|
|
156
|
+
"$LANE" "$LANE" "$MILESTONE_ESCAPED" "$SLUG"
|
|
157
|
+
else
|
|
158
|
+
echo "$LANE"
|
|
159
|
+
fi
|
|
138
160
|
fi
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# forge-run.sh — Universal shell-script entry point for non-Claude agents
|
|
3
|
+
#
|
|
4
|
+
# Usage: forge-run.sh <command> <issue_number> [-R <owner/repo>]
|
|
5
|
+
# command: Pipeline command to run (currently: work-on)
|
|
6
|
+
# issue_number: GitHub issue number (required)
|
|
7
|
+
# -R <owner/repo>: GitHub repository (optional, defaults to current repo)
|
|
8
|
+
#
|
|
9
|
+
# Description:
|
|
10
|
+
# GitHub-state-aware phase router. Reads workflow:* labels and FORGE annotation
|
|
11
|
+
# comments on the given issue to determine the current pipeline phase, then emits
|
|
12
|
+
# structured JSON events to stdout describing phase state and transitions.
|
|
13
|
+
#
|
|
14
|
+
# This script does NOT invoke LLM agents — it reports what phase the pipeline is
|
|
15
|
+
# in and what the next action should be. It is designed to be called by any
|
|
16
|
+
# agent runtime that can execute bash (Aider /run, Cursor terminal, CI runners,
|
|
17
|
+
# OpenCode shell) to get deterministic routing information without re-implementing
|
|
18
|
+
# the phase detection logic.
|
|
19
|
+
#
|
|
20
|
+
# JSON Event Schema:
|
|
21
|
+
# Each event is a single JSON object on its own line (newline-delimited JSON):
|
|
22
|
+
#
|
|
23
|
+
# Phase detection:
|
|
24
|
+
# {"event":"phase_detected","command":"work-on","issue":N,"phase":"<phase>","ts":"<ISO8601>"}
|
|
25
|
+
#
|
|
26
|
+
# Phase detail:
|
|
27
|
+
# {"event":"phase_detail","phase":"<phase>","lane":"<lane>","branch":"<branch>","labels":["<label>",...]}
|
|
28
|
+
#
|
|
29
|
+
# Action required:
|
|
30
|
+
# {"event":"action_required","phase":"<phase>","action":"<description>","ts":"<ISO8601>"}
|
|
31
|
+
#
|
|
32
|
+
# Terminal state:
|
|
33
|
+
# {"event":"terminal","phase":"<phase>","label":"<terminal-label>","issue":N,"ts":"<ISO8601>"}
|
|
34
|
+
#
|
|
35
|
+
# Error:
|
|
36
|
+
# {"event":"error","code":"<error_code>","message":"<message>","ts":"<ISO8601>"}
|
|
37
|
+
#
|
|
38
|
+
# Phase values (work-on command):
|
|
39
|
+
# no-comments — No investigation started; next: Phase 1 (investigate)
|
|
40
|
+
# investigating — Investigation in progress (partial comment, no INVESTIGATION:COMPLETE)
|
|
41
|
+
# ready-to-build — Investigation complete; next: Phase 3 (build)
|
|
42
|
+
# building — Build in progress (no FORGE:BUILDER comment yet)
|
|
43
|
+
# awaiting-pr — Builder complete, no PR; next: Phase 4 (create PR)
|
|
44
|
+
# in-review — PR exists, review in progress; next: Phase 5 (auto-review)
|
|
45
|
+
# merged — Terminal: workflow:merged label set
|
|
46
|
+
# invalid — Terminal: workflow:invalid label set
|
|
47
|
+
# decomposed — Terminal: workflow:decomposed label set (sub-issues spawned)
|
|
48
|
+
# needs-human — Terminal: needs-human label set
|
|
49
|
+
#
|
|
50
|
+
# Exit codes: 0 = success, 1 = error (invalid args, gh auth failure, etc.)
|
|
51
|
+
#
|
|
52
|
+
# <!-- Added: forge#965 -->
|
|
53
|
+
|
|
54
|
+
set -euo pipefail
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Helpers
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
# ISO 8601 timestamp (UTC)
|
|
61
|
+
ts() { date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "1970-01-01T00:00:00Z"; }
|
|
62
|
+
|
|
63
|
+
# Emit a JSON event to stdout (one object per line — NDJSON)
|
|
64
|
+
emit() {
|
|
65
|
+
local json="$1"
|
|
66
|
+
printf '%s\n' "$json"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Emit an error event and exit 1
|
|
70
|
+
die() {
|
|
71
|
+
local code="$1"
|
|
72
|
+
local message="$2"
|
|
73
|
+
local ts_val
|
|
74
|
+
ts_val=$(ts)
|
|
75
|
+
# Escape double quotes in message for JSON embedding
|
|
76
|
+
message="${message//\"/\\\"}"
|
|
77
|
+
emit "{\"event\":\"error\",\"code\":\"${code}\",\"message\":\"${message}\",\"ts\":\"${ts_val}\"}"
|
|
78
|
+
exit 1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# JSON-escape a string (double-quote characters only — sufficient for ASCII label/branch values)
|
|
82
|
+
json_str() {
|
|
83
|
+
local s="$1"
|
|
84
|
+
s="${s//\\/\\\\}"
|
|
85
|
+
s="${s//\"/\\\"}"
|
|
86
|
+
printf '%s' "$s"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
# Argument parsing
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
COMMAND="${1:-}"
|
|
94
|
+
ISSUE_NUMBER="${2:-}"
|
|
95
|
+
GH_REPO_ARGS=()
|
|
96
|
+
|
|
97
|
+
if [ -z "$COMMAND" ]; then
|
|
98
|
+
die "MISSING_COMMAND" "command is required. Usage: forge-run.sh <command> <issue_number> [-R <owner/repo>]"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
if [ -z "$ISSUE_NUMBER" ]; then
|
|
102
|
+
die "MISSING_ISSUE" "issue_number is required. Usage: forge-run.sh <command> <issue_number> [-R <owner/repo>]"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Validate command
|
|
106
|
+
case "$COMMAND" in
|
|
107
|
+
work-on) ;;
|
|
108
|
+
*)
|
|
109
|
+
die "UNKNOWN_COMMAND" "unknown command: ${COMMAND}. Supported: work-on"
|
|
110
|
+
;;
|
|
111
|
+
esac
|
|
112
|
+
|
|
113
|
+
# Validate issue number is numeric
|
|
114
|
+
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
|
|
115
|
+
die "INVALID_ISSUE" "issue_number must be numeric, got: ${ISSUE_NUMBER}"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
# Parse remaining flags
|
|
119
|
+
shift 2
|
|
120
|
+
while [ $# -gt 0 ]; do
|
|
121
|
+
case "$1" in
|
|
122
|
+
-R)
|
|
123
|
+
if [ $# -lt 2 ]; then
|
|
124
|
+
die "MISSING_REPO" "-R requires a value <owner/repo>. Usage: forge-run.sh <command> <issue_number> [-R <owner/repo>]"
|
|
125
|
+
fi
|
|
126
|
+
if ! [[ "$2" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
|
|
127
|
+
die "INVALID_REPO" "-R value must be owner/repo format, got: ${2}"
|
|
128
|
+
fi
|
|
129
|
+
GH_REPO_ARGS=(-R "$2")
|
|
130
|
+
shift 2
|
|
131
|
+
;;
|
|
132
|
+
*)
|
|
133
|
+
die "UNKNOWN_FLAG" "unknown argument: ${1}. Usage: forge-run.sh <command> <issue_number> [-R <owner/repo>]"
|
|
134
|
+
;;
|
|
135
|
+
esac
|
|
136
|
+
done
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# Locate scripts directory (where classify-lane.sh lives alongside this script)
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
142
|
+
CLASSIFY_LANE="${SCRIPT_DIR}/classify-lane.sh"
|
|
143
|
+
|
|
144
|
+
if [ ! -x "$CLASSIFY_LANE" ]; then
|
|
145
|
+
die "MISSING_SCRIPT" "classify-lane.sh not found or not executable at: ${CLASSIFY_LANE}"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Phase 1: Read issue state from GitHub
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
# Fetch issue: state, labels
|
|
153
|
+
GH_STDERR_TMP=$(mktemp)
|
|
154
|
+
ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" "${GH_REPO_ARGS[@]}" \
|
|
155
|
+
--json number,state,labels,title \
|
|
156
|
+
2>"$GH_STDERR_TMP") || {
|
|
157
|
+
MSG=$(cat "$GH_STDERR_TMP")
|
|
158
|
+
rm -f "$GH_STDERR_TMP"
|
|
159
|
+
die "GH_FETCH_FAILED" "failed to fetch issue #${ISSUE_NUMBER}: ${MSG}"
|
|
160
|
+
}
|
|
161
|
+
rm -f "$GH_STDERR_TMP"
|
|
162
|
+
|
|
163
|
+
# Extract state and labels
|
|
164
|
+
ISSUE_STATE=$(printf '%s' "$ISSUE_JSON" | jq -r '.state')
|
|
165
|
+
LABELS_JSON=$(printf '%s' "$ISSUE_JSON" | jq -c '[.labels[].name]')
|
|
166
|
+
ISSUE_TITLE=$(printf '%s' "$ISSUE_JSON" | jq -r '.title')
|
|
167
|
+
|
|
168
|
+
# Check if issue is closed
|
|
169
|
+
if [ "$ISSUE_STATE" = "CLOSED" ]; then
|
|
170
|
+
ts_val=$(ts)
|
|
171
|
+
emit "{\"event\":\"terminal\",\"phase\":\"closed\",\"label\":\"closed\",\"issue\":${ISSUE_NUMBER},\"ts\":\"${ts_val}\"}"
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Phase 2: Detect terminal labels
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
TERMINAL_LABEL=""
|
|
180
|
+
for label in "workflow:merged" "workflow:invalid" "workflow:decomposed" "needs-human"; do
|
|
181
|
+
if printf '%s' "$LABELS_JSON" | jq -e --arg l "$label" 'contains([$l])' > /dev/null 2>&1; then
|
|
182
|
+
TERMINAL_LABEL="$label"
|
|
183
|
+
break
|
|
184
|
+
fi
|
|
185
|
+
done
|
|
186
|
+
|
|
187
|
+
if [ -n "$TERMINAL_LABEL" ]; then
|
|
188
|
+
ts_val=$(ts)
|
|
189
|
+
emit "{\"event\":\"terminal\",\"phase\":\"${TERMINAL_LABEL}\",\"label\":\"$(json_str "${TERMINAL_LABEL}")\",\"issue\":${ISSUE_NUMBER},\"ts\":\"${ts_val}\"}"
|
|
190
|
+
exit 0
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# Phase 3: Read FORGE annotation comments to determine pipeline phase
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
# gh issue view with --json comments is the reliable approach: it accepts the -R flag
|
|
198
|
+
# via GH_REPO_ARGS and handles both default-repo and explicit-repo cases.
|
|
199
|
+
# gh api repos/{owner}/{repo}/... would need GH_REPO env var for -R override — avoided here.
|
|
200
|
+
GH_STDERR_TMP=$(mktemp)
|
|
201
|
+
COMMENTS_JSON=$(gh issue view "$ISSUE_NUMBER" "${GH_REPO_ARGS[@]}" \
|
|
202
|
+
--json comments --jq '[.comments[] | {id: .id, body: .body}]' \
|
|
203
|
+
2>"$GH_STDERR_TMP") || {
|
|
204
|
+
MSG=$(cat "$GH_STDERR_TMP")
|
|
205
|
+
rm -f "$GH_STDERR_TMP"
|
|
206
|
+
die "GH_COMMENTS_FAILED" "failed to fetch comments for issue #${ISSUE_NUMBER}: ${MSG}"
|
|
207
|
+
}
|
|
208
|
+
rm -f "$GH_STDERR_TMP" 2>/dev/null || true
|
|
209
|
+
|
|
210
|
+
if [ -z "$COMMENTS_JSON" ]; then
|
|
211
|
+
COMMENTS_JSON="[]"
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
# Detect annotation presence
|
|
215
|
+
has_annotation() {
|
|
216
|
+
local marker="$1"
|
|
217
|
+
printf '%s' "$COMMENTS_JSON" | jq -e --arg m "$marker" 'any(.[]; .body | contains($m))' > /dev/null 2>&1
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Detect complete annotation (has both the annotation tag AND its completion sentinel)
|
|
221
|
+
is_annotation_complete() {
|
|
222
|
+
local open_tag="$1"
|
|
223
|
+
local sentinel="$2"
|
|
224
|
+
printf '%s' "$COMMENTS_JSON" | jq -e \
|
|
225
|
+
--arg o "$open_tag" --arg s "$sentinel" \
|
|
226
|
+
'any(.[]; (.body | contains($o)) and (.body | contains($s)))' > /dev/null 2>&1
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
INVESTIGATOR_COMPLETE=false
|
|
230
|
+
BUILDER_COMPLETE=false
|
|
231
|
+
BUILDER_PRESENT=false
|
|
232
|
+
REVIEW_STARTED=false
|
|
233
|
+
|
|
234
|
+
if is_annotation_complete "FORGE:INVESTIGATOR" "INVESTIGATION:COMPLETE"; then
|
|
235
|
+
INVESTIGATOR_COMPLETE=true
|
|
236
|
+
fi
|
|
237
|
+
if has_annotation "FORGE:BUILDER"; then
|
|
238
|
+
BUILDER_PRESENT=true
|
|
239
|
+
fi
|
|
240
|
+
if is_annotation_complete "FORGE:BUILDER" "FORGE:BUILDER:COMPLETE"; then
|
|
241
|
+
BUILDER_COMPLETE=true
|
|
242
|
+
fi
|
|
243
|
+
if has_annotation "FORGE:REVIEW_STARTED"; then
|
|
244
|
+
REVIEW_STARTED=true
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Phase 4: Determine current workflow phase from labels + annotations
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
# Read current workflow labels
|
|
252
|
+
WORKFLOW_LABEL=""
|
|
253
|
+
for label in "workflow:in-review" "workflow:building" "workflow:ready-to-build" "workflow:investigating"; do
|
|
254
|
+
if printf '%s' "$LABELS_JSON" | jq -e --arg l "$label" 'contains([$l])' > /dev/null 2>&1; then
|
|
255
|
+
WORKFLOW_LABEL="$label"
|
|
256
|
+
break
|
|
257
|
+
fi
|
|
258
|
+
done
|
|
259
|
+
|
|
260
|
+
# Phase detection logic (matches work-on.md Phase 0B: "Determine resume point")
|
|
261
|
+
PHASE="no-comments"
|
|
262
|
+
ACTION=""
|
|
263
|
+
|
|
264
|
+
if [ "$REVIEW_STARTED" = "true" ]; then
|
|
265
|
+
PHASE="in-review"
|
|
266
|
+
ACTION="Review in progress — /review-pr handles merge. Check PR state."
|
|
267
|
+
elif [ "$BUILDER_COMPLETE" = "true" ] && [ "$WORKFLOW_LABEL" != "workflow:in-review" ]; then
|
|
268
|
+
PHASE="awaiting-pr"
|
|
269
|
+
ACTION="Builder complete. Create PR targeting the classified lane branch."
|
|
270
|
+
elif [ "$BUILDER_PRESENT" = "true" ] && [ "$BUILDER_COMPLETE" = "false" ]; then
|
|
271
|
+
PHASE="building"
|
|
272
|
+
ACTION="Builder comment present but incomplete. Resume build from FORGE:BUILDER comment."
|
|
273
|
+
elif [ "$WORKFLOW_LABEL" = "workflow:building" ]; then
|
|
274
|
+
PHASE="building"
|
|
275
|
+
ACTION="Build phase active. No FORGE:BUILDER comment yet — build not complete."
|
|
276
|
+
elif [ "$INVESTIGATOR_COMPLETE" = "true" ] || [ "$WORKFLOW_LABEL" = "workflow:ready-to-build" ]; then
|
|
277
|
+
PHASE="ready-to-build"
|
|
278
|
+
ACTION="Investigation complete. Invoke /work-on ${ISSUE_NUMBER} to continue to build phase."
|
|
279
|
+
elif has_annotation "FORGE:INVESTIGATOR" && [ "$INVESTIGATOR_COMPLETE" = "false" ]; then
|
|
280
|
+
PHASE="investigating"
|
|
281
|
+
ACTION="Investigation comment present but incomplete (no INVESTIGATION:COMPLETE sentinel). Delete partial comment and restart investigation."
|
|
282
|
+
elif [ "$WORKFLOW_LABEL" = "workflow:investigating" ]; then
|
|
283
|
+
PHASE="investigating"
|
|
284
|
+
ACTION="Investigation in progress. No FORGE:INVESTIGATOR comment yet."
|
|
285
|
+
else
|
|
286
|
+
PHASE="no-comments"
|
|
287
|
+
ACTION="No pipeline activity detected. Invoke /work-on ${ISSUE_NUMBER} to start investigation."
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# Phase 5: Classify lane (for informational output in phase_detail event)
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
LANE=""
|
|
295
|
+
BRANCH=""
|
|
296
|
+
LANE_JSON=""
|
|
297
|
+
if LANE_JSON=$("$CLASSIFY_LANE" "$ISSUE_NUMBER" "${GH_REPO_ARGS[@]}" --json 2>/dev/null); then
|
|
298
|
+
LANE=$(printf '%s' "$LANE_JSON" | jq -r '.lane // empty')
|
|
299
|
+
BRANCH=$(printf '%s' "$LANE_JSON" | jq -r '.branch // empty')
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
# Phase 6: Emit events
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
ts_val=$(ts)
|
|
307
|
+
|
|
308
|
+
# Event 1: phase_detected
|
|
309
|
+
emit "{\"event\":\"phase_detected\",\"command\":\"$(json_str "${COMMAND}")\",\"issue\":${ISSUE_NUMBER},\"phase\":\"$(json_str "${PHASE}")\",\"ts\":\"${ts_val}\"}"
|
|
310
|
+
|
|
311
|
+
# Event 2: phase_detail
|
|
312
|
+
LABELS_STR=$(printf '%s' "$LABELS_JSON")
|
|
313
|
+
emit "{\"event\":\"phase_detail\",\"phase\":\"$(json_str "${PHASE}")\",\"lane\":\"$(json_str "${LANE}")\",\"branch\":\"$(json_str "${BRANCH}")\",\"labels\":${LABELS_STR}}"
|
|
314
|
+
|
|
315
|
+
# Event 3: action_required (only for non-terminal active phases)
|
|
316
|
+
if [ -n "$ACTION" ]; then
|
|
317
|
+
ts_val=$(ts)
|
|
318
|
+
ACTION_ESCAPED="${ACTION//\"/\\\"}"
|
|
319
|
+
emit "{\"event\":\"action_required\",\"phase\":\"$(json_str "${PHASE}")\",\"action\":\"${ACTION_ESCAPED}\",\"ts\":\"${ts_val}\"}"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
exit 0
|