forgedock 1.1.11 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +9 -3
  2. package/bin/cli-spawn-shared.mjs +25 -0
  3. package/bin/engine/phases.mjs +178 -28
  4. package/bin/engine/state.mjs +39 -7
  5. package/bin/engine-cli.mjs +85 -3
  6. package/bin/engine.mjs +71 -8
  7. package/bin/forgedock.mjs +322 -20
  8. package/bin/hooks/pre-tool-use.mjs +345 -10
  9. package/bin/init-enrich-api.mjs +12 -4
  10. package/bin/init-enrich-cli.mjs +328 -0
  11. package/bin/init-enrich.mjs +137 -0
  12. package/bin/journey.mjs +116 -15
  13. package/bin/runner.mjs +653 -10
  14. package/commands/adopt.md +1 -1
  15. package/commands/analytics.md +10 -7
  16. package/commands/audit-agents.md +29 -6
  17. package/commands/audit.md +9 -20
  18. package/commands/autopilot.md +52 -78
  19. package/commands/changelog.md +1 -1
  20. package/commands/ci-audit.md +19 -10
  21. package/commands/cleanup.md +1 -1
  22. package/commands/compat-audit.md +1 -1
  23. package/commands/deploy-info.md +1 -1
  24. package/commands/deploy-pr.md +1 -1
  25. package/commands/design-bench.md +1 -1
  26. package/commands/design-render-critique-loop.md +1 -1
  27. package/commands/diagnose.md +1 -1
  28. package/commands/forge-stats.md +6 -5
  29. package/commands/forgedock-init.md +1 -1
  30. package/commands/geo-audit.md +9 -6
  31. package/commands/incident-response.md +15 -5
  32. package/commands/issue.md +302 -15
  33. package/commands/milestone.md +45 -18
  34. package/commands/optimize.md +1 -1
  35. package/commands/orchestrate/phase-1-resolve.md +20 -5
  36. package/commands/orchestrate/phase-4-execution.md +8 -1
  37. package/commands/orchestrate/phase-5-cleanup.md +43 -2
  38. package/commands/orchestrate/phase-6-report.md +1 -0
  39. package/commands/orchestrate.md +1 -1
  40. package/commands/pipeline-health/phase-4-proposals.md +40 -10
  41. package/commands/pipeline-health/phase-5-report.md +140 -13
  42. package/commands/pipeline-health.md +1 -1
  43. package/commands/pipeline-resume.md +1 -1
  44. package/commands/pipeline-status.md +1 -1
  45. package/commands/qa-sweep.md +27 -4
  46. package/commands/recover-orphans.md +1 -1
  47. package/commands/replay.md +16 -2
  48. package/commands/review-pr-agents/protocols.md +4 -0
  49. package/commands/review-pr-agents/spec-cli.md +179 -0
  50. package/commands/review-pr-agents.md +7 -4
  51. package/commands/review-pr-staging.md +44 -15
  52. package/commands/review-pr.md +122 -28
  53. package/commands/rollback.md +15 -5
  54. package/commands/security-audit.md +14 -11
  55. package/commands/signal-planner.md +32 -12
  56. package/commands/spec-doctor.md +1 -1
  57. package/commands/sync-ecosystem.md +54 -11
  58. package/commands/test-gate.md +49 -11
  59. package/commands/upgrade-deps.md +518 -0
  60. package/commands/validate.md +16 -5
  61. package/commands/work-on/build/architect.md +1 -1
  62. package/commands/work-on/build/context.md +1 -1
  63. package/commands/work-on/build/implement.md +2 -2
  64. package/commands/work-on/build/validate.md +64 -2
  65. package/commands/work-on/build.md +10 -2
  66. package/commands/work-on/close.md +1 -1
  67. package/commands/work-on/decompose.md +35 -25
  68. package/commands/work-on/investigate.md +2 -2
  69. package/commands/work-on/remediate.md +1 -1
  70. package/commands/work-on/review.md +5 -3
  71. package/commands/work-on-monolithic.md +1 -1
  72. package/commands/work-on.md +28 -7
  73. package/docs/AIDER.md +264 -0
  74. package/docs/AUTOPILOT-CRON.md +130 -0
  75. package/docs/CI.md +18 -0
  76. package/docs/CONFIG.md +41 -2
  77. package/docs/CURSOR.md +135 -0
  78. package/docs/OPENCODE.md +312 -0
  79. package/docs/demo.gif +0 -0
  80. package/docs/eval/README.md +4 -4
  81. package/docs/launch/show-hn.md +3 -3
  82. package/docs/site/annotations-explained.md +2 -2
  83. package/docs/site/architecture.md +180 -0
  84. package/docs/site/command-reference.md +38 -0
  85. package/docs/site/configuration.md +243 -0
  86. package/docs/site/for-companies.md +4 -4
  87. package/docs/site/forge-annotation-protocol.md +1 -1
  88. package/docs/site/github-is-the-memory.md +2 -2
  89. package/docs/site/how-it-works.md +1 -1
  90. package/docs/site/public/llms-full.txt +380 -0
  91. package/docs/site/public/llms.txt +35 -0
  92. package/docs/spec/review-protocol.md +6 -0
  93. package/docs/spec-graph-schema.md +3 -3
  94. package/package.json +2 -2
  95. package/packages/protocol/README.md +75 -0
  96. package/packages/protocol/src/card.js +114 -0
  97. package/packages/protocol/src/cli.js +4 -77
  98. package/packages/protocol/src/emit.js +14 -3
  99. package/packages/protocol/src/index.js +2 -1
  100. package/packages/protocol/src/parse.js +17 -8
  101. package/packages/protocol/src/types.js +1 -1
  102. package/packages/protocol/src/validate.js +11 -0
  103. package/scripts/check-command-side-effects.sh +261 -0
  104. package/scripts/check-spec-markers.sh +273 -0
  105. package/scripts/classify-lane.sh +29 -7
  106. package/scripts/forge-run.sh +322 -0
  107. package/templates/cross-repo-trigger.yml +282 -0
  108. package/templates/cursor/forge.mdc +148 -0
  109. 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
@@ -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
- echo "$STAGING_BRANCH"
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
- echo "$LANE"
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