forgedock 1.0.16 → 1.0.18
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 +54 -3
- package/bin/cross-publish.mjs +344 -0
- package/bin/forgedock.mjs +2205 -541
- package/bin/github-app-manifest.json.license +2 -0
- package/bin/hooks/session-start.mjs +4 -5
- package/bin/registry.mjs +20 -0
- package/bin/tui.mjs +172 -96
- package/commands/adopt.md +433 -0
- package/commands/analytics.md +2 -0
- package/commands/audit-agents.md +2 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +77 -0
- package/commands/changelog.md +354 -0
- package/commands/cleanup.md +2 -0
- package/commands/compat-audit.md +330 -0
- package/commands/deploy-info.md +6 -5
- package/commands/design-bench.md +176 -0
- package/commands/design-render-critique-loop.md +103 -0
- package/commands/design.md +259 -0
- package/commands/diagnose.md +483 -0
- package/commands/explain.md +349 -0
- package/commands/forge-stats.md +2 -0
- package/commands/forgedock-init.md +2 -0
- package/commands/geo-audit.md +2 -0
- package/commands/incident-response.md +2 -0
- package/commands/issue.md +9 -3
- package/commands/milestone.md +2 -0
- package/commands/optimize.md +411 -0
- package/commands/orchestrate.md +239 -0
- package/commands/pipeline-health.md +60 -1
- package/commands/qa-sweep.md +39 -2
- package/commands/quality-gate.md +67 -0
- package/commands/replay.md +263 -0
- package/commands/resume.md +264 -0
- package/commands/review-pr-agents.md +5 -1
- package/commands/review-pr-staging.md +160 -2
- package/commands/review-pr.md +85 -3
- package/commands/rollback.md +2 -0
- package/commands/scope.md +280 -0
- package/commands/security-audit.md +2 -0
- package/commands/status.md +262 -0
- package/commands/sync-ecosystem.md +2 -0
- package/commands/test-gate.md +929 -0
- package/commands/validate.md +2 -0
- package/commands/work-on/build/architect.md +128 -18
- package/commands/work-on/build/context.md +130 -17
- package/commands/work-on/build/implement.md +7 -3
- package/commands/work-on/build/validate.md +2 -0
- package/commands/work-on/build.md +32 -7
- package/commands/work-on/close.md +85 -0
- package/commands/work-on/decompose.md +2 -0
- package/commands/work-on/investigate.md +30 -3
- package/commands/work-on/review.md +13 -1
- package/commands/work-on-monolithic.md +2 -0
- package/commands/work-on.md +676 -38
- package/docs/BRAND.md +51 -0
- package/docs/CI.md +177 -0
- package/docs/CONFIG.md +94 -0
- package/docs/FORGE-PROTOCOL.md +324 -0
- package/docs/articles/b-vs-c-baseline.md +210 -0
- package/docs/articles/full-abc-benchmark.md +205 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +174 -0
- package/docs/articles/selective-spec-loading-token-savings.md +140 -0
- package/docs/claude-breakpoints.json +47 -0
- package/docs/design/abc-benchmark.md +195 -0
- package/docs/design/design-architect-rationale.md +123 -0
- package/docs/design/design-memory.md +61 -0
- package/docs/design/design-spec-schema.md +248 -0
- package/docs/design/design-system-lint.md +150 -0
- package/docs/design/divergent-generation.md +106 -0
- package/docs/design/effects-appropriateness.md +133 -0
- package/docs/design/fixtures/briefs/README.md +47 -0
- package/docs/design/fixtures/briefs/cadence.md +39 -0
- package/docs/design/fixtures/briefs/plume.md +39 -0
- package/docs/design/fixtures/briefs/slipstream.md +39 -0
- package/docs/design/fixtures/briefs/tender.md +39 -0
- package/docs/design/fixtures/briefs/voltage.md +39 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/runs.json +175 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/scorecard.json +138 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/b-vs-c-baseline/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-1.html +562 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-2.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-a/run-3.html +618 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-1.html +364 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-2.html +420 -0
- package/docs/design/fixtures/runs/full-abc/cadence/arm-b/run-3.html +506 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-1.html +527 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-2.html +445 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-a/run-3.html +483 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-1.html +611 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-2.html +594 -0
- package/docs/design/fixtures/runs/full-abc/plume/arm-b/run-3.html +817 -0
- package/docs/design/fixtures/runs/full-abc/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-1.html +496 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-2.html +447 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-a/run-3.html +564 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-1.html +373 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-2.html +398 -0
- package/docs/design/fixtures/runs/full-abc/slipstream/arm-b/run-3.html +595 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-1.html +622 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-2.html +475 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-a/run-3.html +660 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-1.html +531 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-2.html +565 -0
- package/docs/design/fixtures/runs/full-abc/tender/arm-b/run-3.html +674 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-1.html +525 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-a/run-3.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-1.html +388 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-2.html +463 -0
- package/docs/design/fixtures/runs/full-abc/voltage/arm-b/run-3.html +568 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-1.html +1068 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-2.html +1423 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-a/run-3.html +1245 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-1.html +305 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-2.html +327 -0
- package/docs/design/fixtures/runs/full-abc-opus/cadence/arm-b/run-3.html +428 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-1.html +1270 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-2.html +1172 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-a/run-3.html +1532 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-1.html +315 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-2.html +342 -0
- package/docs/design/fixtures/runs/full-abc-opus/plume/arm-b/run-3.html +353 -0
- package/docs/design/fixtures/runs/full-abc-opus/runs.json +190 -0
- package/docs/design/fixtures/runs/full-abc-opus/scorecard.json +144 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-1.html +345 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-a/run-3.html +502 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-1.html +408 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-2.html +446 -0
- package/docs/design/fixtures/runs/full-abc-opus/slipstream/arm-b/run-3.html +496 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-1.html +813 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-2.html +711 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-a/run-3.html +721 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-1.html +280 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-2.html +291 -0
- package/docs/design/fixtures/runs/full-abc-opus/tender/arm-b/run-3.html +366 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-1.html +1010 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-2.html +836 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-a/run-3.html +930 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-1.html +407 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-2.html +396 -0
- package/docs/design/fixtures/runs/full-abc-opus/voltage/arm-b/run-3.html +529 -0
- package/docs/design/foundations/brutalist.css +294 -0
- package/docs/design/foundations/editorial.css +300 -0
- package/docs/design/foundations/minimal-luxury.css +295 -0
- package/docs/design/foundations/technical.css +293 -0
- package/docs/design/foundations/warm-photo.css +299 -0
- package/docs/design/reference-corpus.md +1726 -0
- package/docs/design/render-critique-loop.md +57 -0
- package/docs/site/getting-started.md +21 -0
- package/docs/spec-graph-schema.md +397 -0
- package/package.json +3 -3
- package/scripts/bench-scorecard.mjs +239 -0
- package/scripts/build-spec-graph.mjs +509 -0
- package/scripts/classify-lane.sh +138 -0
- package/scripts/design-system-lint.mjs +993 -0
- package/scripts/graph-query.sh +486 -0
- package/scripts/transition-label.sh +169 -0
- package/scripts/validate-pr-target.sh +30 -0
- package/scripts/validate-spec-graph.sh +423 -0
- package/templates/devdocs/README.md +72 -0
- package/templates/devdocs/agent/using-forgedock.md +3 -0
- package/templates/devdocs/agent/using-github.md +3 -0
- package/templates/devdocs/index.yaml +162 -0
- package/templates/devdocs/project/architecture.md +3 -0
- package/templates/devdocs/project/conventions.md +3 -0
- package/templates/devdocs/project/custom-instructions.md +3 -0
- package/templates/devdocs/project/glossary.md +3 -0
- package/templates/devdocs/project/stack.md +3 -0
- package/templates/devdocs/templates/commit-convention.md +3 -0
- package/templates/devdocs/templates/issue.md +3 -0
- package/templates/devdocs/templates/pull-request.md +3 -0
- package/templates/devdocs/templates/review-finding.md +3 -0
- package/templates/workflows/forgedock-review.yml +96 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Analyze this repo and generate per-repo adaptive scripts from learned patterns, git history, and existing configuration
|
|
3
|
+
argument-hint: [--dry-run | --force | --no-registry]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /optimize — Per-Repo Adaptive Script Generation
|
|
7
|
+
|
|
8
|
+
**Input**: $ARGUMENTS
|
|
9
|
+
|
|
10
|
+
Proactively analyze the current repository and generate `.forgedock/scripts/` entries from `forge.yaml → learned:`, git commit history, and pipeline annotations — without waiting for a correction event. This is the proactive half of the learning loop; reactive capture happens in `/work-on` Phase 1D.
|
|
11
|
+
|
|
12
|
+
**Agent model policy**: Default `model: "sonnet"`. Fallback: `model: "opus"` if Sonnet is rate-limited.
|
|
13
|
+
**NEVER use plan mode (EnterPlanMode).**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Argument Parsing
|
|
18
|
+
|
|
19
|
+
| Flag | Effect |
|
|
20
|
+
|------|--------|
|
|
21
|
+
| (none) | Full run: analyze → generate → registry → report |
|
|
22
|
+
| `--dry-run` | Analyze and report what would be generated; write nothing to disk |
|
|
23
|
+
| `--force` | Overwrite scripts even if confidence < 0.7 |
|
|
24
|
+
| `--no-registry` | Skip `registry.json` update |
|
|
25
|
+
|
|
26
|
+
Parse `$ARGUMENTS` and set:
|
|
27
|
+
```
|
|
28
|
+
DRY_RUN = true if --dry-run present, else false
|
|
29
|
+
FORCE = true if --force present, else false
|
|
30
|
+
NO_REG = true if --no-registry present, else false
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
### P1: Read forge.yaml
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
REPO_PATH=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
|
41
|
+
FORGE_YAML="$REPO_PATH/forge.yaml"
|
|
42
|
+
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$FORGE_YAML" 2>/dev/null)
|
|
43
|
+
SCRIPTS_DIR="$REPO_PATH/.forgedock/scripts"
|
|
44
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // true' "$FORGE_YAML" 2>/dev/null || echo "true")
|
|
45
|
+
ADAPTIVE_COMMIT=$(yq '.adaptive_scripts.commit // false' "$FORGE_YAML" 2>/dev/null || echo "false")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If `$FORGE_YAML` is missing:
|
|
49
|
+
```
|
|
50
|
+
STOP: forge.yaml not found. Run `npx forgedock init` to generate it, then re-run /optimize.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If `ADAPTIVE_ENABLED` is `false`:
|
|
54
|
+
```
|
|
55
|
+
STOP: adaptive_scripts.enabled is false in forge.yaml. Set it to true to use /optimize.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### P2: Ensure scripts directory exists
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
mkdir -p "$SCRIPTS_DIR"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If `.forgedock/scripts/` is not gitignored, note in the report (non-blocking).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Phase 1: Pattern Analysis
|
|
69
|
+
|
|
70
|
+
Read all available signal sources. Build a `PATTERNS` map — keyed by script name, value is `{evidence, confidence, value}`.
|
|
71
|
+
|
|
72
|
+
**Confidence scale**:
|
|
73
|
+
- `1.0` — explicit value in `forge.yaml → learned:` (authoritative)
|
|
74
|
+
- `0.85` — value confirmed by 3+ independent signals (git history, FORGE:LEARNED annotations)
|
|
75
|
+
- `0.7` — value confirmed by 2 independent signals
|
|
76
|
+
- `0.5` — single signal (heuristic only)
|
|
77
|
+
- `<0.5` — insufficient evidence; skip unless `--force`
|
|
78
|
+
|
|
79
|
+
### 1A: Read forge.yaml → learned: section
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
LEARNED=$(yq '.learned // {}' "$FORGE_YAML" 2>/dev/null || echo '{}')
|
|
83
|
+
echo "Learned section: $LEARNED"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Extract each known key:
|
|
87
|
+
```bash
|
|
88
|
+
LEARNED_STAGING=$(yq '.learned.branch_targets.staging // ""' "$FORGE_YAML" 2>/dev/null)
|
|
89
|
+
LEARNED_TEST_CMDS=$(yq '.learned.test_commands // []' "$FORGE_YAML" -o json 2>/dev/null)
|
|
90
|
+
LEARNED_TEST_LOCS=$(yq '.learned.test_locations // []' "$FORGE_YAML" -o json 2>/dev/null)
|
|
91
|
+
LEARNED_LABEL_MAP=$(yq '.learned.label_map // {}' "$FORGE_YAML" -o json 2>/dev/null)
|
|
92
|
+
LEARNED_COMMIT_STYLE=$(yq '.learned.commit_style // ""' "$FORGE_YAML" 2>/dev/null)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For each non-empty learned key, add to PATTERNS with confidence `1.0`:
|
|
96
|
+
|
|
97
|
+
| Non-empty key | Script to generate |
|
|
98
|
+
|---------------|--------------------|
|
|
99
|
+
| `branch_targets.staging` | `branch-targets.sh` |
|
|
100
|
+
| `test_commands` (non-empty array) | `run-tests.sh` |
|
|
101
|
+
| `test_locations` (non-empty array) | `find-tests.sh` |
|
|
102
|
+
| `label_map` (non-empty object) | `label-map.sh` |
|
|
103
|
+
| `commit_style` | `format-commit.sh` |
|
|
104
|
+
|
|
105
|
+
### 1B: Analyze git commit history
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cd "$REPO_PATH"
|
|
109
|
+
|
|
110
|
+
# Sample last 50 commits for style analysis
|
|
111
|
+
COMMIT_SUBJECTS=$(git log --oneline -50 --format="%s" 2>/dev/null | head -50)
|
|
112
|
+
echo "$COMMIT_SUBJECTS"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Detect commit style**:
|
|
116
|
+
```bash
|
|
117
|
+
CONV_COUNT=$(echo "$COMMIT_SUBJECTS" | grep -cE '^(feat|fix|docs|chore|refactor|test|style|ci|build|perf)\(' 2>/dev/null || echo 0)
|
|
118
|
+
CONV_SIMPLE=$(echo "$COMMIT_SUBJECTS" | grep -cE '^(feat|fix|docs|chore|refactor|test|style|ci|build|perf):' 2>/dev/null || echo 0)
|
|
119
|
+
TOTAL=$(echo "$COMMIT_SUBJECTS" | wc -l | tr -d ' ')
|
|
120
|
+
|
|
121
|
+
CONV_WITH_SCOPE_PCT=$(( (CONV_COUNT * 100) / (TOTAL + 1) ))
|
|
122
|
+
CONV_SIMPLE_PCT=$(( (CONV_SIMPLE * 100) / (TOTAL + 1) ))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Determine `DETECTED_COMMIT_STYLE`:
|
|
126
|
+
- If `CONV_WITH_SCOPE_PCT >= 60` → `"conventional-with-scope"`, confidence `0.85`
|
|
127
|
+
- If `CONV_SIMPLE_PCT >= 60` → `"conventional"`, confidence `0.85`
|
|
128
|
+
- If `(CONV_COUNT + CONV_SIMPLE) < 20%` → `"plain"`, confidence `0.7`
|
|
129
|
+
- Otherwise → insufficient signal, skip `format-commit.sh` unless already in `learned:`
|
|
130
|
+
|
|
131
|
+
**If `LEARNED_COMMIT_STYLE` is non-empty**: use it (confidence `1.0`), skip git analysis for this signal.
|
|
132
|
+
|
|
133
|
+
**Detect test locations** (if `LEARNED_TEST_LOCS` is empty):
|
|
134
|
+
```bash
|
|
135
|
+
# Look for test directories with actual test files
|
|
136
|
+
TEST_DIRS=""
|
|
137
|
+
for candidate in tests/ __tests__/ test/ spec/ src/__tests__/; do
|
|
138
|
+
COUNT=$(git ls-files "$candidate" 2>/dev/null | wc -l | tr -d ' ')
|
|
139
|
+
if [ "$COUNT" -gt "0" ]; then
|
|
140
|
+
TEST_DIRS="$TEST_DIRS $candidate"
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
TEST_DIRS=$(echo "$TEST_DIRS" | xargs) # trim
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If `TEST_DIRS` is non-empty and differs from the default (`tests/`): add to PATTERNS for `find-tests.sh` with confidence `0.7`.
|
|
147
|
+
|
|
148
|
+
### 1C: Mine FORGE:LEARNED annotations from closed issues
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Read closed issues for repeated FORGE:LEARNED annotations (last 30 closed issues)
|
|
152
|
+
FORGE_LEARNED_BODIES=$(gh api "repos/$GH_REPO/issues?state=closed&per_page=30" \
|
|
153
|
+
--jq '[.[] | .number]' 2>/dev/null | jq -r '.[]' 2>/dev/null | \
|
|
154
|
+
while read -r n; do
|
|
155
|
+
gh api "repos/$GH_REPO/issues/$n/comments" \
|
|
156
|
+
--jq '.[] | select(.body | contains("FORGE:LEARNED")) | .body' 2>/dev/null
|
|
157
|
+
done | head -200)
|
|
158
|
+
echo "$FORGE_LEARNED_BODIES" | head -50
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Extract corroborating signals from annotations**:
|
|
162
|
+
- Count occurrences of each staging branch override, test command, label remap, commit style across annotations
|
|
163
|
+
- Each repeated annotation (≥2 occurrences) raises confidence by `+0.15` for the matching pattern
|
|
164
|
+
- Cap confidence at `1.0`
|
|
165
|
+
|
|
166
|
+
**Practical limit**: Process at most 30 issues. If `gh` rate-limits, skip 1C and note in report.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Phase 2: Script Generation
|
|
171
|
+
|
|
172
|
+
For each pattern in PATTERNS with `confidence >= 0.7` (or all if `--force`):
|
|
173
|
+
|
|
174
|
+
**Overwrite mode**: if a script already exists, read it first, then overwrite — never append. Log "updated" vs "created" in the report.
|
|
175
|
+
|
|
176
|
+
### 2A: branch-targets.sh
|
|
177
|
+
|
|
178
|
+
Trigger: `branch_targets.staging` set in `learned:` (confidence `1.0`), OR git history shows consistent use of a non-default staging branch.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
cat > "$SCRIPTS_DIR/branch-targets.sh" << 'SCRIPT_EOF'
|
|
182
|
+
#!/usr/bin/env bash
|
|
183
|
+
# Auto-generated by /optimize — branch target overrides for this repo.
|
|
184
|
+
# Source: forge.yaml → learned.branch_targets
|
|
185
|
+
# Edit manually if your branch structure changes.
|
|
186
|
+
|
|
187
|
+
# STAGING_BRANCH: override the default staging branch for fast-lane PRs
|
|
188
|
+
echo "STAGING_BRANCH={LEARNED_STAGING_VALUE}"
|
|
189
|
+
SCRIPT_EOF
|
|
190
|
+
chmod +x "$SCRIPTS_DIR/branch-targets.sh"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Replace `{LEARNED_STAGING_VALUE}` with the actual value from `learned.branch_targets.staging`.
|
|
194
|
+
|
|
195
|
+
### 2B: run-tests.sh
|
|
196
|
+
|
|
197
|
+
Trigger: `test_commands` array non-empty in `learned:` (confidence `1.0`), OR FORGE:LEARNED annotations contain repeated test command corrections.
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
cat > "$SCRIPTS_DIR/run-tests.sh" << 'SCRIPT_EOF'
|
|
201
|
+
#!/usr/bin/env bash
|
|
202
|
+
# Auto-generated by /optimize — test command sequence for this repo.
|
|
203
|
+
# Source: forge.yaml → learned.test_commands
|
|
204
|
+
# Edit manually if your test setup changes.
|
|
205
|
+
|
|
206
|
+
set -e
|
|
207
|
+
|
|
208
|
+
{LEARNED_TEST_COMMANDS_EXPANDED}
|
|
209
|
+
SCRIPT_EOF
|
|
210
|
+
chmod +x "$SCRIPTS_DIR/run-tests.sh"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Expand `{LEARNED_TEST_COMMANDS_EXPANDED}` to one command per line, in array order.
|
|
214
|
+
|
|
215
|
+
### 2C: find-tests.sh
|
|
216
|
+
|
|
217
|
+
Trigger: `test_locations` array non-empty in `learned:` (confidence `1.0`), OR git ls-files shows non-default test directories (confidence `0.7`).
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
cat > "$SCRIPTS_DIR/find-tests.sh" << 'SCRIPT_EOF'
|
|
221
|
+
#!/usr/bin/env bash
|
|
222
|
+
# Auto-generated by /optimize — test file discovery for this repo.
|
|
223
|
+
# Source: forge.yaml → learned.test_locations
|
|
224
|
+
# Returns a list of test files matching this repo's actual test directory layout.
|
|
225
|
+
|
|
226
|
+
set -e
|
|
227
|
+
|
|
228
|
+
{FIND_COMMANDS_EXPANDED}
|
|
229
|
+
SCRIPT_EOF
|
|
230
|
+
chmod +x "$SCRIPTS_DIR/find-tests.sh"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Expand `{FIND_COMMANDS_EXPANDED}` to `find {dir} -name "*.test.*" -o -name "*.spec.*" 2>/dev/null` for each location, plus glob patterns if specified. Patterns ending in `*` are expanded with `git ls-files`.
|
|
234
|
+
|
|
235
|
+
### 2D: label-map.sh
|
|
236
|
+
|
|
237
|
+
Trigger: `label_map` non-empty in `learned:` (confidence `1.0`).
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
cat > "$SCRIPTS_DIR/label-map.sh" << 'SCRIPT_EOF'
|
|
241
|
+
#!/usr/bin/env bash
|
|
242
|
+
# Auto-generated by /optimize — label name remapping for this repo.
|
|
243
|
+
# Source: forge.yaml → learned.label_map
|
|
244
|
+
# Usage: source this file, then use map_label() to resolve canonical → repo label.
|
|
245
|
+
|
|
246
|
+
# Associative array: canonical ForgeDock label → repo-specific label
|
|
247
|
+
declare -A LABEL_MAP=(
|
|
248
|
+
{LABEL_MAP_ENTRIES}
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
map_label() {
|
|
252
|
+
local canonical="$1"
|
|
253
|
+
echo "${LABEL_MAP[$canonical]:-$canonical}"
|
|
254
|
+
}
|
|
255
|
+
SCRIPT_EOF
|
|
256
|
+
chmod +x "$SCRIPTS_DIR/label-map.sh"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Expand `{LABEL_MAP_ENTRIES}` to `["canonical"]="repo-specific"` pairs.
|
|
260
|
+
|
|
261
|
+
### 2E: format-commit.sh
|
|
262
|
+
|
|
263
|
+
Trigger: `commit_style` set in `learned:` (confidence `1.0`), OR git history analysis produces `confidence >= 0.7`.
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
cat > "$SCRIPTS_DIR/format-commit.sh" << 'SCRIPT_EOF'
|
|
267
|
+
#!/usr/bin/env bash
|
|
268
|
+
# Auto-generated by /optimize — commit message style enforcer for this repo.
|
|
269
|
+
# Source: forge.yaml → learned.commit_style (or git history analysis)
|
|
270
|
+
# Usage: ./format-commit.sh "type" "scope" "description" "issue_number"
|
|
271
|
+
# type: conventional prefix (feat, fix, refactor, docs, chore, etc.)
|
|
272
|
+
# scope: component or area being changed (e.g. auth, pipeline, commands)
|
|
273
|
+
# description: short imperative summary of the change
|
|
274
|
+
# issue_number: GitHub issue number (without #)
|
|
275
|
+
# Outputs: the correctly-formatted commit message for this repo.
|
|
276
|
+
|
|
277
|
+
set -e
|
|
278
|
+
|
|
279
|
+
STYLE="{COMMIT_STYLE}"
|
|
280
|
+
TYPE="${1:-}"
|
|
281
|
+
SCOPE="${2:-}"
|
|
282
|
+
DESCRIPTION="${3:-}"
|
|
283
|
+
ISSUE="${4:-}"
|
|
284
|
+
|
|
285
|
+
case "$STYLE" in
|
|
286
|
+
conventional-with-scope)
|
|
287
|
+
if [ -n "$SCOPE" ]; then
|
|
288
|
+
echo "${TYPE}(${SCOPE}): ${DESCRIPTION} (#${ISSUE})"
|
|
289
|
+
else
|
|
290
|
+
echo "${TYPE:-chore}: ${DESCRIPTION} (#${ISSUE})"
|
|
291
|
+
fi
|
|
292
|
+
;;
|
|
293
|
+
conventional)
|
|
294
|
+
echo "${TYPE:-chore}: ${DESCRIPTION} (#${ISSUE})"
|
|
295
|
+
;;
|
|
296
|
+
plain)
|
|
297
|
+
echo "${DESCRIPTION} (#${ISSUE})"
|
|
298
|
+
;;
|
|
299
|
+
*)
|
|
300
|
+
echo "${DESCRIPTION}"
|
|
301
|
+
;;
|
|
302
|
+
esac
|
|
303
|
+
SCRIPT_EOF
|
|
304
|
+
chmod +x "$SCRIPTS_DIR/format-commit.sh"
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Replace `{COMMIT_STYLE}` with the detected or learned value.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Phase 3: Registry Update
|
|
312
|
+
|
|
313
|
+
Skip if `--no-registry`.
|
|
314
|
+
|
|
315
|
+
Write (overwrite) `.forgedock/scripts/registry.json`:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
GENERATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
319
|
+
|
|
320
|
+
cat > "$SCRIPTS_DIR/registry.json" << JSON_EOF
|
|
321
|
+
{
|
|
322
|
+
"schema_version": 1,
|
|
323
|
+
"generated_at": "${GENERATED_AT}",
|
|
324
|
+
"generated_by": "/optimize",
|
|
325
|
+
"scripts": {
|
|
326
|
+
{REGISTRY_ENTRIES}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
JSON_EOF
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Build `{REGISTRY_ENTRIES}` from the PATTERNS map. For each generated script:
|
|
333
|
+
```json
|
|
334
|
+
"branch-targets": {
|
|
335
|
+
"path": "branch-targets.sh",
|
|
336
|
+
"confidence": 1.0,
|
|
337
|
+
"evidence": "forge.yaml → learned.branch_targets.staging",
|
|
338
|
+
"generated_at": "2026-03-15T10:30:00Z"
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Comma-separate all entries. If `--dry-run`: print the registry JSON to stdout instead of writing it.
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Phase 4: Report
|
|
347
|
+
|
|
348
|
+
Output a human-readable summary. Do NOT commit anything — `adaptive_scripts.commit` is respected:
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
echo ""
|
|
352
|
+
echo "=== /optimize Report ==="
|
|
353
|
+
echo "Run at: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
354
|
+
echo "Repo: $GH_REPO"
|
|
355
|
+
echo "Scripts dir: $SCRIPTS_DIR"
|
|
356
|
+
if [ "$DRY_RUN" = "true" ]; then
|
|
357
|
+
echo "Mode: DRY RUN — no files written"
|
|
358
|
+
fi
|
|
359
|
+
echo ""
|
|
360
|
+
echo "--- Patterns detected ---"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
For each pattern in PATTERNS (sorted by confidence descending):
|
|
364
|
+
```
|
|
365
|
+
{script_name}.sh confidence={confidence} source={evidence} action={created|updated|skipped|would-create}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
For patterns with `confidence < 0.7` that were skipped:
|
|
369
|
+
```
|
|
370
|
+
{script_name}.sh SKIPPED (confidence={confidence} < 0.7 — run with --force to generate)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Then:
|
|
374
|
+
```
|
|
375
|
+
--- Summary ---
|
|
376
|
+
Scripts created: N
|
|
377
|
+
Scripts updated: N
|
|
378
|
+
Scripts skipped: N
|
|
379
|
+
Registry: {written|skipped}
|
|
380
|
+
Commit: NOT done (adaptive_scripts.commit=false — review scripts before committing)
|
|
381
|
+
|
|
382
|
+
--- Next steps ---
|
|
383
|
+
1. Review generated scripts in .forgedock/scripts/
|
|
384
|
+
2. Run each script manually to verify output
|
|
385
|
+
3. If correct: git add .forgedock/scripts/ && git commit -m "feat(scripts): adaptive scripts from /optimize"
|
|
386
|
+
4. If not: edit scripts directly or update forge.yaml → learned: and re-run /optimize
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
If no patterns were detected at all:
|
|
390
|
+
```
|
|
391
|
+
No patterns detected with sufficient confidence.
|
|
392
|
+
|
|
393
|
+
Possible reasons:
|
|
394
|
+
- forge.yaml → learned: is empty (no corrections captured yet)
|
|
395
|
+
- Git history is too short (< 10 commits) to detect commit style
|
|
396
|
+
- Test directories match the default (tests/) — no override needed
|
|
397
|
+
|
|
398
|
+
Run /work-on on a few issues to let the pipeline capture corrections, then re-run /optimize.
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## Idempotency Guarantee
|
|
404
|
+
|
|
405
|
+
Running `/optimize` twice produces the same output. Specifically:
|
|
406
|
+
- Scripts are always overwritten, never appended to
|
|
407
|
+
- `registry.json` is always overwritten with current analysis results
|
|
408
|
+
- Generated timestamps in scripts and registry will update on each run (this is expected)
|
|
409
|
+
- Content of scripts changes only if the underlying patterns change
|
|
410
|
+
|
|
411
|
+
To verify: run `/optimize` twice and compare outputs — the only differences should be the `generated_at` timestamps.
|
package/commands/orchestrate.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
description: Orchestrate parallel work on multiple issues or an entire milestone — spawns sub-agents that each run the full /work-on pipeline
|
|
3
3
|
argument-hint: [milestone <slug> | #1 #2 #3 | next <N> | fast-lane | priority:P0]
|
|
4
4
|
---
|
|
5
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
5
7
|
|
|
6
8
|
# /orchestrate — Multi-Issue Parallel Orchestrator
|
|
7
9
|
|
|
@@ -550,6 +552,103 @@ If `UNEXPECTED_GROWTH > 500`, report the alert clearly before launching the next
|
|
|
550
552
|
|
|
551
553
|
---
|
|
552
554
|
|
|
555
|
+
### Step 4A.pre.0: Pre-create milestone branches for the wave (MANDATORY before classify-lane) <!-- Added: forge#901 -->
|
|
556
|
+
|
|
557
|
+
**WHY THIS EXISTS**: Feature-lane milestone branches were created lazily — by whichever feature-lane agent reached its build phase first. When a wave launches N feature-lane agents in parallel, they each run the lane check at roughly the same time. Every agent that runs before the branch is first pushed observes "branch absent" and is misrouted (hard-fail / `needs-human`, or fallback to staging in older code paths). The result is a single milestone's PRs scattered across the milestone branch and staging — a branch-routing nondeterminism that recurs under parallelism.
|
|
558
|
+
|
|
559
|
+
The fix is deterministic: create every milestone branch the wave will target **once, up front, before any agent runs `classify-lane.sh`**. After this step, every agent's lane check sees the branch and routes consistently.
|
|
560
|
+
|
|
561
|
+
**When to run**: Before the classify-lane loop in Step 4A.pre, for every wave. The step is a no-op for pure fast-lane waves (no issue in the wave has a milestone).
|
|
562
|
+
|
|
563
|
+
**Requires bash 4+**: This snippet uses an associative array (`declare -A SEEN_MILESTONE_SLUG`) to de-dupe milestone slugs, so it must run under bash 4 or newer. Under a non-bash POSIX shell (`sh`/dash), `declare -A` fails and the de-dupe silently no-ops. This degrades gracefully — branch creation stays correct because the `git ls-remote --exit-code` exists-check below still skips any milestone branch that already exists; the only effect is redundant, idempotent `ls-remote`/`push` attempts for milestones referenced by more than one issue in the wave. Run this command's blocks under bash 4+. <!-- Added: forge#901 -->
|
|
564
|
+
|
|
565
|
+
```bash
|
|
566
|
+
# Pre-create the origin milestone branch for every distinct milestone referenced by the wave.
|
|
567
|
+
# Slugification MUST byte-match scripts/classify-lane.sh — otherwise a branch is created that
|
|
568
|
+
# the classifier will not select. Keep these two slug pipelines identical.
|
|
569
|
+
git fetch origin
|
|
570
|
+
|
|
571
|
+
# Collect distinct milestone titles among the wave's issues
|
|
572
|
+
declare -A SEEN_MILESTONE_SLUG
|
|
573
|
+
for NUM in {wave_issue_numbers}; do
|
|
574
|
+
MILESTONE_TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null || echo "")
|
|
575
|
+
[ -z "$MILESTONE_TITLE" ] && continue # fast-lane issue — no milestone branch needed
|
|
576
|
+
|
|
577
|
+
# Slugify — IDENTICAL to classify-lane.sh: lowercase → spaces-to-hyphens →
|
|
578
|
+
# strip non-[a-z0-9-] → collapse hyphens → strip leading/trailing hyphens.
|
|
579
|
+
SLUG=$(echo "$MILESTONE_TITLE" \
|
|
580
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
581
|
+
| tr ' ' '-' \
|
|
582
|
+
| tr -cd 'a-z0-9-' \
|
|
583
|
+
| sed 's/--*/-/g' \
|
|
584
|
+
| sed 's/^-//;s/-$//')
|
|
585
|
+
|
|
586
|
+
# Empty-slug guard (matches classify-lane.sh): a title with no ASCII letters/digits/hyphens
|
|
587
|
+
# would produce "milestone/", an invalid ref. Skip and let classify-lane surface the error.
|
|
588
|
+
if [ -z "$SLUG" ]; then
|
|
589
|
+
echo "WARN: milestone title '$MILESTONE_TITLE' (issue #$NUM) produced an empty slug — skipping pre-creation; classify-lane will hard-fail." >&2
|
|
590
|
+
continue
|
|
591
|
+
fi
|
|
592
|
+
|
|
593
|
+
# De-dupe: only attempt creation once per milestone
|
|
594
|
+
[ -n "${SEEN_MILESTONE_SLUG[$SLUG]:-}" ] && continue
|
|
595
|
+
SEEN_MILESTONE_SLUG[$SLUG]=1
|
|
596
|
+
|
|
597
|
+
LANE="milestone/$SLUG"
|
|
598
|
+
if git ls-remote --exit-code origin "$LANE" >/dev/null 2>&1; then
|
|
599
|
+
echo "Milestone branch '$LANE' already exists on origin — no action."
|
|
600
|
+
continue
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
# Create-if-absent from the default branch (matches /milestone create).
|
|
604
|
+
# $DEFAULT_BRANCH is resolved from forge.yaml at the top of this command.
|
|
605
|
+
echo "Pre-creating milestone branch '$LANE' from origin/$DEFAULT_BRANCH …"
|
|
606
|
+
if git push origin "origin/$DEFAULT_BRANCH:refs/heads/$LANE" 2>/dev/null; then
|
|
607
|
+
echo "Created milestone branch '$LANE'."
|
|
608
|
+
elif git ls-remote --exit-code origin "$LANE" >/dev/null 2>&1; then
|
|
609
|
+
# A concurrent orchestrator (or an agent) created it first — harmless. Never force-push.
|
|
610
|
+
echo "Milestone branch '$LANE' was created concurrently — proceeding with the existing branch."
|
|
611
|
+
else
|
|
612
|
+
echo "ERROR: failed to pre-create milestone branch '$LANE' from origin/$DEFAULT_BRANCH." >&2
|
|
613
|
+
echo " classify-lane.sh will hard-fail for issues in this milestone until the branch exists." >&2
|
|
614
|
+
fi
|
|
615
|
+
done
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
This step is the deterministic counterpart to fix #2 (atomic create-if-absent in the classifier): by guaranteeing the branch exists before the wave's lane checks run, no agent can observe a missing branch. `classify-lane.sh`'s hard-fail is intentionally preserved as the phantom-slug gate for any path that bypasses this step.
|
|
619
|
+
|
|
620
|
+
### Step 4A.pre: Classify lane for each issue (MANDATORY before spawning agents)
|
|
621
|
+
|
|
622
|
+
Before building agent prompts, run `classify-lane.sh` for every issue in the current wave to compute `{LANE}` and `{PR_BASE}` deterministically. The script output is authoritative — the LLM MUST NOT override or reason around it.
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
# Requires classify-lane.sh to be available at ~/.claude/scripts/classify-lane.sh
|
|
626
|
+
# (installed by `npx forgedock` — see bin/forgedock.mjs linkScripts step)
|
|
627
|
+
# Fallback: bash "$FORGE_HOME/scripts/classify-lane.sh" if ~/.claude/scripts/ is unavailable
|
|
628
|
+
|
|
629
|
+
declare -A ISSUE_LANE
|
|
630
|
+
declare -A ISSUE_PR_BASE
|
|
631
|
+
|
|
632
|
+
for NUM in {wave_issue_numbers}; do
|
|
633
|
+
PR_BASE=$(bash ~/.claude/scripts/classify-lane.sh "$NUM" -R {GH_REPO}) || {
|
|
634
|
+
echo "ERROR: classify-lane.sh failed for #$NUM — adding needs-human label and skipping" >&2
|
|
635
|
+
gh issue edit "$NUM" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
636
|
+
continue
|
|
637
|
+
}
|
|
638
|
+
# Derive LANE label from PR_BASE
|
|
639
|
+
if [ "$PR_BASE" = "staging" ]; then
|
|
640
|
+
LANE="fast-lane"
|
|
641
|
+
else
|
|
642
|
+
LANE="feature-lane"
|
|
643
|
+
fi
|
|
644
|
+
ISSUE_LANE[$NUM]="$LANE"
|
|
645
|
+
ISSUE_PR_BASE[$NUM]="$PR_BASE"
|
|
646
|
+
echo "#$NUM → lane=$LANE, PR_BASE=$PR_BASE"
|
|
647
|
+
done
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Use `${ISSUE_LANE[$NUM]}` and `${ISSUE_PR_BASE[$NUM]}` to populate `{LANE}` and `{PR_BASE}` in the agent template below. Never substitute prose guesses for these values — the script output is the only valid source. <!-- Added: forge#677 -->
|
|
651
|
+
|
|
553
652
|
### Step 4A: Launch a wave
|
|
554
653
|
|
|
555
654
|
**REMINDER: You MUST use the template below verbatim. Only fill in `{VARIABLES}`. Do NOT rewrite the agent prompt. Do NOT write custom implementation instructions. The agent MUST invoke `/work-on` via the Skill tool — this is the HARD RULE from the top of this file.**
|
|
@@ -694,6 +793,90 @@ You will be automatically notified when each background agent completes. **Do NO
|
|
|
694
793
|
- Reading agent JSONL output files to check progress — use GitHub labels as the source of truth
|
|
695
794
|
- Polling the same status check repeatedly on a timer
|
|
696
795
|
|
|
796
|
+
### Step 4B.5: Time-Based Stall Detection
|
|
797
|
+
|
|
798
|
+
**Purpose**: Catches agents that have stopped responding WITHOUT exiting (e.g., rate-limited, context-frozen, or silently hung). The reactive check in Step 4B only fires on agent completion — this check catches agents that never complete at all.
|
|
799
|
+
|
|
800
|
+
**When to run** (NOT a sleep loop — two trigger points only):
|
|
801
|
+
1. On every background agent completion event (run BEFORE the terminal-state check in Step 4B)
|
|
802
|
+
2. Before posting any "waiting for wave..." status update to the user
|
|
803
|
+
|
|
804
|
+
**Do NOT poll on a timer. Do NOT use sleep. Run at these two trigger points only.**
|
|
805
|
+
|
|
806
|
+
**Read stall timeout from config**:
|
|
807
|
+
```bash
|
|
808
|
+
STALL_TIMEOUT=$(yq '.pipeline.stall_timeout_minutes // 15' forge.yaml 2>/dev/null || echo 15)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**For each non-terminal agent in the current wave**:
|
|
812
|
+
```bash
|
|
813
|
+
for NUM in {active_wave_issue_numbers}; do
|
|
814
|
+
# Skip issues already in terminal state
|
|
815
|
+
TERMINAL=$(gh issue view $NUM -R {GH_REPO} --json labels \
|
|
816
|
+
--jq '[.labels[].name | select(. == "workflow:merged" or . == "workflow:invalid" or . == "needs-human")] | length')
|
|
817
|
+
[ "$TERMINAL" -gt 0 ] && continue
|
|
818
|
+
|
|
819
|
+
# Get last activity timestamp — prefer last comment (catches FORGE:HEARTBEAT updates)
|
|
820
|
+
LAST_ACTIVITY=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
821
|
+
--jq '.[-1].updated_at // empty' 2>/dev/null)
|
|
822
|
+
# Fall back to issue updated_at if no comments
|
|
823
|
+
if [ -z "$LAST_ACTIVITY" ]; then
|
|
824
|
+
LAST_ACTIVITY=$(gh issue view $NUM -R {GH_REPO} --json updatedAt --jq '.updatedAt')
|
|
825
|
+
fi
|
|
826
|
+
|
|
827
|
+
# Compute elapsed minutes (GNU date — adjust for macOS: date -j -f "%Y-%m-%dT%H:%M:%SZ")
|
|
828
|
+
LAST_EPOCH=$(date -d "$LAST_ACTIVITY" +%s 2>/dev/null \
|
|
829
|
+
|| date -j -f "%Y-%m-%dT%H:%M:%SZ" "$LAST_ACTIVITY" +%s 2>/dev/null)
|
|
830
|
+
NOW_EPOCH=$(date +%s)
|
|
831
|
+
ELAPSED_MIN=$(( (NOW_EPOCH - LAST_EPOCH) / 60 ))
|
|
832
|
+
|
|
833
|
+
if [ "$ELAPSED_MIN" -gt "$STALL_TIMEOUT" ]; then
|
|
834
|
+
# Count prior stall events on this issue
|
|
835
|
+
STALL_COUNT=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
836
|
+
--jq '[.[] | select(.body | contains("FORGE:STALL_DETECTED"))] | length')
|
|
837
|
+
|
|
838
|
+
CURRENT_STATE=$(gh issue view $NUM -R {GH_REPO} --json labels \
|
|
839
|
+
--jq '[.labels[].name | select(startswith("workflow:"))] | .[0] // "unknown"')
|
|
840
|
+
|
|
841
|
+
if [ "$STALL_COUNT" -lt 2 ]; then
|
|
842
|
+
# Auto-resume: post stall annotation and re-invoke /work-on
|
|
843
|
+
RESUME_ATTEMPT=$(( STALL_COUNT + 1 ))
|
|
844
|
+
gh issue comment $NUM -R {GH_REPO} --body "<!-- FORGE:STALL_DETECTED -->
|
|
845
|
+
## Stall Detected
|
|
846
|
+
|
|
847
|
+
**Issue**: #${NUM}
|
|
848
|
+
**Elapsed since last activity**: ${ELAPSED_MIN} min (threshold: ${STALL_TIMEOUT} min)
|
|
849
|
+
**Current workflow state**: ${CURRENT_STATE}
|
|
850
|
+
**Auto-resume attempt**: ${RESUME_ATTEMPT} of 2
|
|
851
|
+
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
852
|
+
|
|
853
|
+
# Resume the agent — collect all resumes and launch in a single message (see Step 4B rule)
|
|
854
|
+
# STALL_RESUME_LIST is accumulated and launched in parallel after the loop
|
|
855
|
+
STALL_RESUME_LIST="$STALL_RESUME_LIST $NUM"
|
|
856
|
+
else
|
|
857
|
+
# 2+ prior stalls — auto-resume exhausted, escalate to needs-human
|
|
858
|
+
gh issue edit $NUM -R {GH_REPO} --add-label "needs-human"
|
|
859
|
+
gh issue comment $NUM -R {GH_REPO} --body "<!-- FORGE:STALL_DETECTED -->
|
|
860
|
+
## Stall Escalated — Needs Human Intervention
|
|
861
|
+
|
|
862
|
+
Issue #${NUM} has been auto-resumed ${STALL_COUNT} times without reaching a terminal state. Auto-resume limit (2) exhausted. Manual intervention required.
|
|
863
|
+
|
|
864
|
+
**Last workflow state**: ${CURRENT_STATE}
|
|
865
|
+
**Total elapsed since last activity**: ${ELAPSED_MIN} min
|
|
866
|
+
**Timestamp**: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
867
|
+
echo "STALL ESCALATED: #{NUM} → needs-human (${STALL_COUNT} prior resumes)"
|
|
868
|
+
fi
|
|
869
|
+
fi
|
|
870
|
+
done
|
|
871
|
+
|
|
872
|
+
# Launch all stall resumes in parallel (single message — same rule as Step 4B)
|
|
873
|
+
# For each NUM in $STALL_RESUME_LIST, call Agent(resume="{AGENT_ID}", run_in_background=true, ...)
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
**Resume all stalled agents in a single message** (parallel). Use the same `Agent(resume=...)` pattern as Step 4B — do not wait between individual resumes.
|
|
877
|
+
|
|
878
|
+
**Track stall resume cycles separately** from completion-event resumes (Step 4B). If the same issue accumulates ≥ 2 `FORGE:STALL_DETECTED` comments AND still hasn't reached terminal state, do not resume again — the `needs-human` label is already set.
|
|
879
|
+
|
|
697
880
|
### Step 4C: Collect review-finding issues from completed wave
|
|
698
881
|
|
|
699
882
|
When ALL agents in the current wave have completed, check if any `/work-on` runs spawned review-finding issues during their review phase. These are new work items that should be folded into the remaining waves.
|
|
@@ -814,6 +997,62 @@ They remain open in GitHub — they will be picked up by future `/orchestrate` o
|
|
|
814
997
|
|
|
815
998
|
**If no review-finding issues were spawned:** Continue to next wave normally.
|
|
816
999
|
|
|
1000
|
+
### Step 4C.5: Milestone lane-consistency check (run after each wave) <!-- Added: forge#901 -->
|
|
1001
|
+
|
|
1002
|
+
**WHY THIS EXISTS**: A milestone's feature-lane PRs must all target the same milestone branch. If a branch-routing race ever scatters them — some on the milestone branch, some on staging — the milestone branch becomes incomplete relative to staging, and the split is otherwise invisible until the milestone tries to ship. Step 4A.pre.0 prevents the split deterministically; this check detects any residual split so it surfaces immediately instead of at ship time.
|
|
1003
|
+
|
|
1004
|
+
**When to run**: After each wave completes, for any batch where at least one issue has a milestone. Skip for pure fast-lane batches. This check is **non-blocking** — it alerts; it does not auto-resolve or stop the pipeline.
|
|
1005
|
+
|
|
1006
|
+
```bash
|
|
1007
|
+
# For each distinct milestone in the batch, assert all of its feature-lane PRs share one base.
|
|
1008
|
+
for NUM in {all_batch_issue_numbers}; do
|
|
1009
|
+
MILESTONE_TITLE=$(gh issue view "$NUM" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null || echo "")
|
|
1010
|
+
[ -z "$MILESTONE_TITLE" ] && continue
|
|
1011
|
+
|
|
1012
|
+
SLUG=$(echo "$MILESTONE_TITLE" \
|
|
1013
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
1014
|
+
| tr ' ' '-' \
|
|
1015
|
+
| tr -cd 'a-z0-9-' \
|
|
1016
|
+
| sed 's/--*/-/g' \
|
|
1017
|
+
| sed 's/^-//;s/-$//')
|
|
1018
|
+
[ -z "$SLUG" ] && continue
|
|
1019
|
+
EXPECTED_BASE="milestone/$SLUG"
|
|
1020
|
+
|
|
1021
|
+
# Collect the base branch of every PR that closes an issue in this milestone.
|
|
1022
|
+
# Iterate the milestone's issues and read each one's linked PR base.
|
|
1023
|
+
# Exclude CLOSED-unmerged PRs: a closed-but-not-merged PR is a superseded/abandoned
|
|
1024
|
+
# routing attempt and does NOT reflect the live lane. Keep only OPEN (in-flight) and
|
|
1025
|
+
# MERGED (landed) PRs. `gh pr list --state` cannot combine open+merged, so query all
|
|
1026
|
+
# and drop CLOSED in jq.
|
|
1027
|
+
BASES=$(gh pr list -R {GH_REPO} --state all --search "milestone:\"$MILESTONE_TITLE\"" \
|
|
1028
|
+
--json baseRefName,state --jq '.[] | select(.state != "CLOSED") | .baseRefName' 2>/dev/null | sort -u)
|
|
1029
|
+
# Fallback: if PR search by milestone is unavailable, derive from the issues' linked PRs.
|
|
1030
|
+
if [ -z "$BASES" ]; then
|
|
1031
|
+
BASES=$(for IN in {all_batch_issue_numbers}; do
|
|
1032
|
+
IM=$(gh issue view "$IN" -R {GH_REPO} --json milestone --jq '.milestone.title // empty' 2>/dev/null)
|
|
1033
|
+
[ "$IM" = "$MILESTONE_TITLE" ] || continue
|
|
1034
|
+
gh pr list -R {GH_REPO} --state all --search "$IN in:body" \
|
|
1035
|
+
--json baseRefName,state --jq '.[] | select(.state != "CLOSED") | .baseRefName' 2>/dev/null
|
|
1036
|
+
done | sort -u)
|
|
1037
|
+
fi
|
|
1038
|
+
|
|
1039
|
+
STRAY_BASES=$(echo "$BASES" | grep -v "^${EXPECTED_BASE}\$" | grep -v '^$' || true)
|
|
1040
|
+
if [ -n "$STRAY_BASES" ]; then
|
|
1041
|
+
echo "ALERT: milestone '$MILESTONE_TITLE' has feature-lane PRs split across multiple base branches." >&2
|
|
1042
|
+
echo " Expected base: $EXPECTED_BASE" >&2
|
|
1043
|
+
echo " Found bases:" >&2
|
|
1044
|
+
echo "$BASES" | sed 's/^/ - /' >&2
|
|
1045
|
+
echo " This indicates a branch-routing split — reconcile the stray PRs onto $EXPECTED_BASE" >&2
|
|
1046
|
+
echo " (rebase/cherry-pick the stray branch onto the milestone branch) before the milestone ships." >&2
|
|
1047
|
+
# Do NOT auto-stop or auto-resolve — surface the alert and let the user decide.
|
|
1048
|
+
else
|
|
1049
|
+
echo "Lane-consistency OK: all '$MILESTONE_TITLE' PRs target $EXPECTED_BASE."
|
|
1050
|
+
fi
|
|
1051
|
+
done
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
Report any `ALERT` lines prominently before advancing to the next wave. Reconciliation of an existing split is a manual/`/milestone`-assisted step — this check only ensures the split is never silent.
|
|
1055
|
+
|
|
817
1056
|
### Step 4D: Milestone integration build gate (MANDATORY between waves)
|
|
818
1057
|
|
|
819
1058
|
**WHY THIS EXISTS**: Session Intelligence milestone shipped 116 PRs across multiple waves with zero integration testing. Each PR built in isolation — type errors from cross-PR interactions (wrong prop types, missing components, incompatible interfaces) were invisible until the milestone→staging merge broke the build with 4 distinct errors. This gate catches those failures early.
|