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.
Files changed (53) hide show
  1. package/README.md +8 -2
  2. package/bin/engine/phases.mjs +178 -28
  3. package/bin/engine/state.mjs +39 -7
  4. package/bin/engine-cli.mjs +63 -2
  5. package/bin/engine.mjs +25 -4
  6. package/bin/forgedock.mjs +235 -4
  7. package/bin/hooks/pre-tool-use.mjs +26 -12
  8. package/bin/journey.mjs +103 -11
  9. package/commands/forge-stats.md +5 -4
  10. package/commands/issue.md +5 -1
  11. package/commands/orchestrate/phase-4-execution.md +2 -0
  12. package/commands/qa-sweep.md +17 -0
  13. package/commands/replay.md +16 -2
  14. package/commands/review-pr-agents/spec-cli.md +179 -0
  15. package/commands/review-pr.md +49 -0
  16. package/commands/upgrade-deps.md +518 -0
  17. package/commands/work-on/build/validate.md +61 -1
  18. package/commands/work-on/build.md +9 -1
  19. package/commands/work-on/decompose.md +8 -1
  20. package/docs/AIDER.md +264 -0
  21. package/docs/AUTOPILOT-CRON.md +130 -0
  22. package/docs/CONFIG.md +36 -0
  23. package/docs/CURSOR.md +135 -0
  24. package/docs/OPENCODE.md +312 -0
  25. package/docs/demo.gif +0 -0
  26. package/docs/eval/README.md +4 -4
  27. package/docs/launch/show-hn.md +3 -3
  28. package/docs/site/architecture.md +180 -0
  29. package/docs/site/command-reference.md +17 -0
  30. package/docs/site/configuration.md +243 -0
  31. package/docs/site/for-companies.md +4 -4
  32. package/docs/site/forge-annotation-protocol.md +1 -1
  33. package/docs/site/github-is-the-memory.md +2 -2
  34. package/docs/site/how-it-works.md +1 -1
  35. package/docs/site/public/llms-full.txt +380 -0
  36. package/docs/site/public/llms.txt +35 -0
  37. package/docs/spec-graph-schema.md +3 -3
  38. package/package.json +2 -2
  39. package/packages/protocol/README.md +75 -0
  40. package/packages/protocol/src/card.js +114 -0
  41. package/packages/protocol/src/cli.js +4 -77
  42. package/packages/protocol/src/emit.js +14 -3
  43. package/packages/protocol/src/index.js +2 -1
  44. package/packages/protocol/src/parse.js +17 -8
  45. package/packages/protocol/src/types.js +1 -1
  46. package/packages/protocol/src/validate.js +11 -0
  47. package/scripts/check-command-side-effects.sh +261 -0
  48. package/scripts/check-spec-markers.sh +273 -0
  49. package/scripts/classify-lane.sh +29 -7
  50. package/scripts/forge-run.sh +322 -0
  51. package/templates/cross-repo-trigger.yml +282 -0
  52. package/templates/cursor/forge.mdc +148 -0
  53. package/templates/github-actions/forgedock-autopilot.yml +113 -0
@@ -0,0 +1,282 @@
1
+ # ForgeDock Cross-Repo Trigger — Workflow Template
2
+ #
3
+ # Installation
4
+ # ─────────────
5
+ # Copy this file into your PARENT repo at:
6
+ # .github/workflows/cross-repo-trigger.yml
7
+ #
8
+ # When a PR merges in this repo, the workflow reads forge.yaml to find satellite
9
+ # repos that have trigger_on_merge: true, checks whether touched files match each
10
+ # satellite's trigger_paths globs, and creates a structured investigation issue in
11
+ # every matching satellite repo.
12
+ #
13
+ # Token requirements
14
+ # ──────────────────
15
+ # GITHUB_TOKEN (provided automatically by GitHub Actions) only has write access
16
+ # to THIS repo. To create issues in satellite repos you need a Personal Access
17
+ # Token (PAT) with "issues: write" scope on each satellite repo:
18
+ #
19
+ # Repo Settings → Secrets and variables → Actions → New repository secret
20
+ # Name: FORGE_PAT
21
+ # Value: ghp_... (your PAT)
22
+ #
23
+ # The workflow uses FORGE_PAT when set; it falls back to GITHUB_TOKEN otherwise.
24
+ # For same-org repos you may not need a PAT if GITHUB_TOKEN already has cross-repo
25
+ # write access (org-level permission inheritance). Cross-org or private satellite
26
+ # repos always require a PAT.
27
+ #
28
+ # forge.yaml configuration
29
+ # ─────────────────────────
30
+ # Each satellite that should receive cross-repo triggers must set trigger_on_merge:
31
+ #
32
+ # repos:
33
+ # satellites:
34
+ # - prefix: "sdk"
35
+ # repo: "acme-org/acme-python-sdk"
36
+ # staging_branch: "main"
37
+ # local_path: "/home/youruser/projects/acme-python-sdk"
38
+ # trigger_on_merge: true
39
+ # trigger_paths:
40
+ # - "src/api/**"
41
+ # - "types/**"
42
+ # auto_run: false # reserved — see ForgeDock docs for auto_run setup
43
+ #
44
+ # Fields:
45
+ # trigger_on_merge — boolean; set true to enable triggering for this satellite
46
+ # trigger_paths — list of glob strings (GitHub Actions glob syntax); trigger
47
+ # fires only when at least one changed file matches; omit to
48
+ # trigger on every merge
49
+ # auto_run — boolean; reserved for future auto_run handler; currently no-op
50
+ #
51
+ # Limits and guards
52
+ # ─────────────────
53
+ # - Max 10 satellite issues created per merge event (rate limit guard)
54
+ # - Circular dependency guard: skips satellites whose repo matches this repo
55
+ # - Satellites with trigger_on_merge: false (or absent) are skipped
56
+ # - If forge.yaml is absent, the workflow exits cleanly with an informational log
57
+
58
+ name: ForgeDock Cross-Repo Trigger
59
+
60
+ on:
61
+ pull_request:
62
+ types: [closed]
63
+
64
+ jobs:
65
+ trigger:
66
+ name: Fire cross-repo satellite triggers
67
+ runs-on: ubuntu-latest
68
+
69
+ # Only run when the PR was actually merged (not just closed)
70
+ if: github.event.pull_request.merged == true
71
+
72
+ permissions:
73
+ contents: read # Required: read forge.yaml from checked-out repo
74
+ issues: write # Required: fallback if this repo is a self-referencing satellite
75
+
76
+ env:
77
+ # Use FORGE_PAT when set (cross-repo access); fall back to GITHUB_TOKEN.
78
+ # Assign via env so it is available to all steps without repeating the expression.
79
+ FORGE_TOKEN: ${{ secrets.FORGE_PAT || github.token }}
80
+
81
+ steps:
82
+ - name: Checkout repository
83
+ uses: actions/checkout@v4
84
+
85
+ - name: Check forge.yaml exists
86
+ id: forge_check
87
+ run: |
88
+ if [ -f "forge.yaml" ]; then
89
+ echo "exists=true" >> "$GITHUB_OUTPUT"
90
+ echo "forge.yaml found"
91
+ else
92
+ echo "exists=false" >> "$GITHUB_OUTPUT"
93
+ echo "forge.yaml not found — no satellite configuration to process. Exiting cleanly."
94
+ fi
95
+
96
+ - name: Parse forge.yaml and get changed files
97
+ id: forge_parse
98
+ if: steps.forge_check.outputs.exists == 'true'
99
+ env:
100
+ GH_TOKEN: ${{ env.FORGE_TOKEN }}
101
+ PR_NUMBER: ${{ github.event.pull_request.number }}
102
+ UPSTREAM_REPO: ${{ github.repository }}
103
+ run: |
104
+ # Get changed files from the merged PR
105
+ CHANGED_FILES_JSON=$(gh pr view "$PR_NUMBER" -R "$UPSTREAM_REPO" --json files --jq '[.files[].path]')
106
+ echo "Changed files: $CHANGED_FILES_JSON"
107
+ echo "changed_files_json=$CHANGED_FILES_JSON" >> "$GITHUB_OUTPUT"
108
+
109
+ # Parse satellite list from forge.yaml using Python3 (always available on ubuntu-latest)
110
+ SATELLITES_JSON=$(python3 -c "
111
+ import yaml, json, sys
112
+
113
+ try:
114
+ with open('forge.yaml', 'r') as f:
115
+ config = yaml.safe_load(f)
116
+ except Exception as e:
117
+ print(json.dumps([]), end='')
118
+ sys.exit(0)
119
+
120
+ repos = config.get('repos', {}) or {}
121
+ satellites = repos.get('satellites', []) or []
122
+
123
+ result = []
124
+ for sat in satellites:
125
+ if not isinstance(sat, dict):
126
+ continue
127
+ repo = sat.get('repo', '')
128
+ trigger_on_merge = sat.get('trigger_on_merge', False)
129
+ trigger_paths = sat.get('trigger_paths', []) or []
130
+ auto_run = sat.get('auto_run', False)
131
+ if not repo or not trigger_on_merge:
132
+ continue
133
+ result.append({
134
+ 'repo': repo,
135
+ 'trigger_paths': trigger_paths,
136
+ 'auto_run': bool(auto_run),
137
+ })
138
+
139
+ print(json.dumps(result), end='')
140
+ ")
141
+ echo "Satellites with trigger_on_merge=true: $SATELLITES_JSON"
142
+ echo "satellites_json=$SATELLITES_JSON" >> "$GITHUB_OUTPUT"
143
+
144
+ - name: Fire satellite triggers
145
+ if: steps.forge_check.outputs.exists == 'true'
146
+ env:
147
+ GH_TOKEN: ${{ env.FORGE_TOKEN }}
148
+ UPSTREAM_REPO: ${{ github.repository }}
149
+ PR_NUMBER: ${{ github.event.pull_request.number }}
150
+ MERGED_BRANCH: ${{ github.event.pull_request.head.ref }}
151
+ PR_URL: ${{ github.event.pull_request.html_url }}
152
+ PR_TITLE: ${{ github.event.pull_request.title }}
153
+ SATELLITES_JSON: ${{ steps.forge_parse.outputs.satellites_json }}
154
+ CHANGED_FILES_JSON: ${{ steps.forge_parse.outputs.changed_files_json }}
155
+ run: |
156
+ python3 -c "
157
+ import json, os, subprocess, sys, fnmatch
158
+
159
+ upstream_repo = os.environ['UPSTREAM_REPO']
160
+ pr_number = os.environ['PR_NUMBER']
161
+ merged_branch = os.environ['MERGED_BRANCH']
162
+ pr_url = os.environ['PR_URL']
163
+ pr_title = os.environ['PR_TITLE']
164
+ satellites_json = os.environ.get('SATELLITES_JSON', '[]')
165
+ changed_files_json = os.environ.get('CHANGED_FILES_JSON', '[]')
166
+
167
+ try:
168
+ satellites = json.loads(satellites_json) if satellites_json else []
169
+ except json.JSONDecodeError:
170
+ satellites = []
171
+
172
+ try:
173
+ changed_files = json.loads(changed_files_json) if changed_files_json else []
174
+ except json.JSONDecodeError:
175
+ changed_files = []
176
+
177
+ if not satellites:
178
+ print('No satellites with trigger_on_merge: true found in forge.yaml')
179
+ sys.exit(0)
180
+
181
+ if not changed_files:
182
+ print('No changed files found in merged PR — nothing to match against trigger_paths')
183
+ sys.exit(0)
184
+
185
+ def matches_glob(path, pattern):
186
+ if fnmatch.fnmatch(path, pattern):
187
+ return True
188
+ if '**' not in pattern:
189
+ return False
190
+ parts = pattern.split('**')
191
+ prefix = parts[0].rstrip('/')
192
+ suffix = parts[-1].lstrip('/')
193
+ if prefix and not path.startswith(prefix):
194
+ return False
195
+ if suffix and not (fnmatch.fnmatch(path, suffix) or '/' + suffix in path):
196
+ return False
197
+ return True
198
+
199
+ issues_created = 0
200
+ MAX_ISSUES = 10
201
+ NL = chr(10)
202
+
203
+ for sat in satellites:
204
+ if issues_created >= MAX_ISSUES:
205
+ print(f'Rate limit reached: {MAX_ISSUES} satellite issues created. Stopping.')
206
+ break
207
+
208
+ sat_repo = sat.get('repo', '')
209
+ trigger_paths = sat.get('trigger_paths', [])
210
+ auto_run = sat.get('auto_run', False)
211
+
212
+ if not sat_repo:
213
+ continue
214
+
215
+ # Circular dependency guard
216
+ if sat_repo == upstream_repo:
217
+ print(f'Skipping {sat_repo}: circular dependency guard (satellite == upstream repo)')
218
+ continue
219
+
220
+ # Glob matching against trigger_paths
221
+ if trigger_paths:
222
+ matched_files = [
223
+ f for f in changed_files
224
+ if any(matches_glob(f, p) for p in trigger_paths)
225
+ ]
226
+ if not matched_files:
227
+ print(f'Skipping {sat_repo}: no changed files match trigger_paths {trigger_paths}')
228
+ continue
229
+ else:
230
+ matched_files = list(changed_files)
231
+ print(f'{sat_repo}: no trigger_paths set — triggering on all {len(matched_files)} changed file(s)')
232
+
233
+ print(f'Creating issue in {sat_repo}: {len(matched_files)} matched file(s)')
234
+
235
+ matched_list = NL.join(f'- {f}' for f in matched_files)
236
+ auto_run_note = (
237
+ NL + '> **auto_run** is set for this satellite. '
238
+ 'Run /work-on on this issue number to begin automated investigation.' + NL
239
+ if auto_run else ''
240
+ )
241
+
242
+ issue_body = (
243
+ '<!-- FORGE:CROSS_REPO_TRIGGER -->' + NL +
244
+ '## Cross-Repo Trigger' + NL + NL +
245
+ f'A PR merged in **{upstream_repo}** touches paths relevant to this satellite repo.' + NL + NL +
246
+ '### Upstream Event' + NL +
247
+ f'- Upstream repo: {upstream_repo}' + NL +
248
+ f'- Merged PR: #{pr_number} ({pr_url})' + NL +
249
+ f'- PR title: {pr_title}' + NL +
250
+ f'- Merged branch: {merged_branch}' + NL + NL +
251
+ '### Matched Files' + NL +
252
+ f'These changed files matched this satellites trigger_paths:' + NL + NL +
253
+ matched_list + NL + NL +
254
+ '### Recommended Action' + NL +
255
+ 'Investigate whether these changes affect this satellite repos contracts, types, or integrations.' + NL + NL +
256
+ f'Review the diff at {pr_url} or run /work-on on this issue to begin automated investigation.' + NL +
257
+ auto_run_note + NL +
258
+ '### Context' + NL +
259
+ 'Created automatically by the ForgeDock cross-repo trigger workflow.' + NL +
260
+ 'See https://github.com/RapierCraftStudios/ForgeDock for documentation.' + NL
261
+ )
262
+
263
+ issue_title = f'feat(cross-repo): upstream merge in {upstream_repo} (PR #{pr_number}) — investigate impact'
264
+
265
+ result = subprocess.run(
266
+ ['gh', 'issue', 'create',
267
+ '-R', sat_repo,
268
+ '--title', issue_title,
269
+ '--body', issue_body],
270
+ capture_output=True, text=True
271
+ )
272
+
273
+ if result.returncode == 0:
274
+ issue_url = result.stdout.strip()
275
+ print(f'Created issue in {sat_repo}: {issue_url}')
276
+ issues_created += 1
277
+ else:
278
+ print(f'ERROR creating issue in {sat_repo}: {result.stderr.strip()}')
279
+ print('Hint: Ensure FORGE_PAT has issues:write access to this satellite repo.')
280
+
281
+ print(f'Done: {issues_created} satellite issue(s) created.')
282
+ "
@@ -0,0 +1,148 @@
1
+ ---
2
+ description: ForgeDock pipeline adapter for Cursor Agent mode
3
+ alwaysApply: false
4
+ ---
5
+
6
+ # ForgeDock Pipeline — Cursor Adapter
7
+
8
+ This file is part of the [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock) autonomous AI development pipeline. It adapts the ForgeDock pipeline for execution within Cursor's Agent mode.
9
+
10
+ ## What ForgeDock Is
11
+
12
+ ForgeDock is a structured development pipeline that uses GitHub as a knowledge graph. Each pipeline phase (investigate → build → review → merge) writes machine-readable annotations (FORGE: HTML comments) to GitHub issue and PR comments. Downstream phases read those annotations to reconstruct context — without relying on conversation history.
13
+
14
+ **Key principle**: GitHub issues/PRs are the persistent context layer. All state lives there, not in this conversation.
15
+
16
+ ## Prerequisites
17
+
18
+ Before running any pipeline phase, verify these tools are available in the terminal:
19
+
20
+ ```bash
21
+ gh auth status # GitHub CLI — authenticated
22
+ git --version # Git — available
23
+ ```
24
+
25
+ ForgeDock command specs live in the repo at `commands/` (if this repo uses ForgeDock) or are available globally after `npx forgedock` install.
26
+
27
+ ## Running the Pipeline
28
+
29
+ ### Start a Pipeline Run
30
+
31
+ To work on a GitHub issue, invoke the `work-on` command spec directly:
32
+
33
+ ```bash
34
+ # Read the full work-on spec first
35
+ cat commands/work-on.md
36
+
37
+ # Then execute the pipeline for issue #NUMBER following the spec exactly
38
+ # Phase sequence: Phase 0 (load) → Phase 1 (investigate) → Phase 3 (build) → Phase 4 (PR) → Phase 5 (review) → Phase 6 (close)
39
+ ```
40
+
41
+ **Critical**: After reading `commands/work-on.md`, follow the Universal Phase Dispatcher section exactly. Execute all phases in sequence — do not stop between phases unless a terminal label is set (`workflow:merged`, `workflow:invalid`, `needs-human`).
42
+
43
+ ### Load Issue Context
44
+
45
+ ```bash
46
+ # Load issue state (always do this first — GitHub is the source of truth)
47
+ gh issue view NUMBER -R OWNER/REPO --json number,title,body,labels,state,comments,milestone
48
+
49
+ # Read existing pipeline annotations
50
+ gh api repos/OWNER/REPO/issues/NUMBER/comments \
51
+ --jq '.[] | {id: .id, author: .user.login, body: .body}'
52
+ ```
53
+
54
+ ### Check Terminal State
55
+
56
+ After every phase, check whether a terminal label is set before continuing:
57
+
58
+ ```bash
59
+ gh issue view NUMBER -R OWNER/REPO --json labels --jq '[.labels[].name]'
60
+ ```
61
+
62
+ **Terminal labels** (stop if any are present):
63
+ - `workflow:merged`
64
+ - `workflow:invalid`
65
+ - `needs-human`
66
+ - `workflow:decomposed`
67
+
68
+ ### Write FORGE Annotations
69
+
70
+ Each pipeline phase writes a structured annotation to GitHub. Use exact format from `docs/FORGE-PROTOCOL.md`:
71
+
72
+ ```bash
73
+ # Post investigation report (Phase 1C)
74
+ gh issue comment NUMBER -R OWNER/REPO --body "<!-- FORGE:INVESTIGATOR -->
75
+ ## Investigation Report
76
+ ...
77
+ <!-- INVESTIGATION:COMPLETE -->"
78
+
79
+ # Post builder report (Phase 3M)
80
+ gh issue comment NUMBER -R OWNER/REPO --body "<!-- FORGE:BUILDER -->
81
+ ## Implementation Complete
82
+ ...
83
+ <!-- FORGE:BUILDER:COMPLETE -->"
84
+ ```
85
+
86
+ ### Git Workflow
87
+
88
+ ```bash
89
+ # Create a worktree for the branch (Phase 3E)
90
+ git fetch origin
91
+ git worktree add .claude/worktrees/BRANCH_NAME -b BRANCH_NAME origin/STAGING_OR_MILESTONE_BRANCH
92
+
93
+ # Commit changes (Phase 3K)
94
+ cd .claude/worktrees/BRANCH_NAME
95
+ git add -u
96
+ git commit -m "feat(scope): description (#NUMBER)"
97
+
98
+ # Push and create PR (Phase 4B-4D)
99
+ git push -u origin BRANCH_NAME
100
+ gh pr create -R OWNER/REPO --base PR_BASE --head BRANCH_NAME \
101
+ --title "Feat: description" --body "..."
102
+ ```
103
+
104
+ ### Labels
105
+
106
+ ForgeDock uses `workflow:*` labels to track pipeline state:
107
+
108
+ ```bash
109
+ # Transition labels as you progress through phases
110
+ gh issue edit NUMBER -R OWNER/REPO --add-label "workflow:investigating"
111
+ gh issue edit NUMBER -R OWNER/REPO --add-label "workflow:ready-to-build" --remove-label "workflow:investigating"
112
+ gh issue edit NUMBER -R OWNER/REPO --add-label "workflow:building" --remove-label "workflow:ready-to-build"
113
+ gh issue edit NUMBER -R OWNER/REPO --add-label "workflow:in-review" --remove-label "workflow:building"
114
+ gh issue edit NUMBER -R OWNER/REPO --add-label "workflow:merged" --remove-label "workflow:in-review"
115
+ ```
116
+
117
+ ## Limitations vs. Claude Code
118
+
119
+ | Capability | Claude Code | Cursor (this adapter) |
120
+ |------------|-------------|----------------------|
121
+ | Skill loading | Dynamic (`~/.claude/commands/`) | Static (this rules file) |
122
+ | Multi-phase pipeline | Automatic (single session) | Manual (re-invoke agent per phase) |
123
+ | Sub-skill invocation | `Skill("review-pr")` | Read `commands/review-pr.md` directly |
124
+ | Session continuity | Native compaction resilience | Re-read GitHub state each invocation |
125
+
126
+ **Multi-phase pipeline**: Cursor Agent mode does not persist between invocations. After each phase completes, you must manually re-invoke the agent for the next phase. The pipeline's compaction resilience (reading GitHub state at the start of each phase) handles this correctly — each new agent invocation re-reads the issue and picks up from the current label state.
127
+
128
+ **Sub-skill invocation**: When the pipeline spec calls `Skill("review-pr")` or similar, read the referenced command spec file directly:
129
+
130
+ ```bash
131
+ cat commands/review-pr.md # Then follow the spec
132
+ ```
133
+
134
+ ## Configuration
135
+
136
+ Your project's ForgeDock configuration is in `forge.yaml`. The pipeline reads `project.github.repo`, `project.github.owner`, and `branches.staging` from this file.
137
+
138
+ ```bash
139
+ cat forge.yaml # Review project configuration
140
+ ```
141
+
142
+ ## Reference
143
+
144
+ - `commands/work-on.md` — Full pipeline spec (investigate → build → review → merge)
145
+ - `commands/review-pr.md` — PR review spec
146
+ - `commands/quality-gate.md` — Pre-commit quality checks
147
+ - `docs/FORGE-PROTOCOL.md` — FORGE annotation protocol reference
148
+ - `docs/CURSOR.md` — Cursor-specific installation and usage guide
@@ -0,0 +1,113 @@
1
+ # ForgeDock Autopilot — Scheduled Cron Workflow Template
2
+ #
3
+ # Copy this file into your repo at:
4
+ # .github/workflows/forgedock-autopilot.yml
5
+ #
6
+ # Required secrets (set in your repo's Settings → Secrets → Actions):
7
+ # ANTHROPIC_API_KEY — Your Anthropic API key (https://console.anthropic.com)
8
+ #
9
+ # GITHUB_TOKEN is provided automatically by GitHub Actions.
10
+ #
11
+ # See docs/AUTOPILOT-CRON.md for full setup guide.
12
+
13
+ name: ForgeDock Autopilot
14
+
15
+ on:
16
+ schedule:
17
+ # Every Monday at 9 AM UTC — adjust to your preferred cadence
18
+ - cron: '0 9 * * 1'
19
+
20
+ workflow_dispatch:
21
+ # Manual trigger — allows you to run autopilot on demand with custom options
22
+ inputs:
23
+ mode:
24
+ description: 'Run mode: recon (safe, read-only) or fix (creates branches + PRs)'
25
+ required: false
26
+ default: 'recon'
27
+ type: choice
28
+ options:
29
+ - recon
30
+ - fix
31
+ limit:
32
+ description: 'Max issues to fix per run (fix mode only)'
33
+ required: false
34
+ default: '3'
35
+ type: string
36
+
37
+ jobs:
38
+ autopilot:
39
+ name: Run /autopilot
40
+ runs-on: ubuntu-latest
41
+
42
+ permissions:
43
+ contents: write # Required: create branches, push commits (fix mode)
44
+ issues: write # Required: create and label issues
45
+ pull-requests: write # Required: create PRs (fix mode)
46
+
47
+ steps:
48
+ - name: Checkout repository
49
+ uses: actions/checkout@v4
50
+ with:
51
+ # Fetch full history so git log commands in /autopilot work correctly
52
+ fetch-depth: 0
53
+
54
+ - name: Set up Node.js
55
+ uses: actions/setup-node@v4
56
+ with:
57
+ node-version: 24
58
+
59
+ - name: Install ForgeDock
60
+ run: npx --yes forgedock
61
+
62
+ - name: Install Claude Code CLI
63
+ run: npm install -g @anthropic-ai/claude-code
64
+
65
+ - name: Resolve autopilot arguments
66
+ id: args
67
+ env:
68
+ # Pass workflow_dispatch inputs through env: indirection rather than
69
+ # interpolating ${{ inputs.* }} directly into the run: script text.
70
+ # inputs.limit is free-text (type: string) — splicing it straight into
71
+ # the shell would let a crafted value break out of the assignment and
72
+ # inject arbitrary commands. inputs.mode is a `type: choice` enum and
73
+ # isn't exploitable the same way, but is routed the same way for
74
+ # consistency/defense-in-depth.
75
+ MODE_INPUT: ${{ inputs.mode }}
76
+ LIMIT_INPUT: ${{ inputs.limit }}
77
+ run: |
78
+ # On scheduled runs: always recon mode (safe default)
79
+ # On workflow_dispatch: use the selected mode input
80
+ if [ "${{ github.event_name }}" = "schedule" ]; then
81
+ MODE="recon"
82
+ LIMIT="3"
83
+ else
84
+ MODE="$MODE_INPUT"
85
+ LIMIT="$LIMIT_INPUT"
86
+ fi
87
+
88
+ if [ "$MODE" = "fix" ]; then
89
+ ARGS="--fix --limit $LIMIT"
90
+ else
91
+ ARGS="--recon-only"
92
+ fi
93
+
94
+ echo "mode=$MODE" >> "$GITHUB_OUTPUT"
95
+ echo "args=$ARGS" >> "$GITHUB_OUTPUT"
96
+ echo "Autopilot mode: $MODE (args: $ARGS)"
97
+
98
+ - name: Run /autopilot
99
+ env:
100
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
101
+ GH_TOKEN: ${{ github.token }}
102
+ # CI=true tells /autopilot Phase 4B to skip the interactive confirmation
103
+ # gate. The workflow_dispatch mode=fix input serves as the human opt-in.
104
+ CI: "true"
105
+ # AUTOPILOT_ARGS is derived (one step upstream) from the free-text
106
+ # inputs.limit workflow_dispatch input. Route it through env:
107
+ # indirection here too — splicing it directly via ${{ }} into this
108
+ # run: block would reopen the same script-injection vector one hop
109
+ # later, in a step that also carries ANTHROPIC_API_KEY/GH_TOKEN.
110
+ AUTOPILOT_ARGS: ${{ steps.args.outputs.args }}
111
+ run: |
112
+ claude --dangerously-skip-permissions \
113
+ -p "/autopilot $AUTOPILOT_ARGS"