forgedock 1.0.22 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +255 -211
- package/bin/batch-runner.mjs +339 -0
- package/bin/cinema.mjs +320 -0
- package/bin/demo.mjs +92 -7
- package/bin/engine/phases.mjs +168 -0
- package/bin/engine/projector.mjs +32 -0
- package/bin/engine/reconcile.mjs +11 -0
- package/bin/engine/runlog.mjs +79 -0
- package/bin/engine/state.mjs +32 -0
- package/bin/engine-cli.mjs +222 -0
- package/bin/engine.mjs +138 -0
- package/bin/forgedock.mjs +896 -1540
- package/bin/hooks/interactive-engine.mjs +509 -0
- package/bin/hooks/pre-tool-use.mjs +498 -0
- package/bin/hooks/subagent-stop-enforce.mjs +197 -0
- package/bin/journey.mjs +841 -0
- package/bin/registry.mjs +139 -0
- package/bin/report.mjs +447 -0
- package/bin/runner.mjs +136 -20
- package/bin/settings-hook.mjs +346 -0
- package/bin/tui.mjs +6 -3
- package/commands/adopt.md +4 -1
- package/commands/analytics.md +4 -1
- package/commands/audit-agents.md +110 -27
- package/commands/audit.md +20 -1
- package/commands/autopilot.md +84 -12
- package/commands/changelog.md +2 -1
- package/commands/ci-audit.md +2 -1
- package/commands/cleanup.md +161 -1
- package/commands/compat-audit.md +2 -1
- package/commands/deploy-info.md +63 -4
- package/commands/design-bench.md +6 -1
- package/commands/design-render-critique-loop.md +2 -1
- package/commands/design.md +1 -0
- package/commands/diagnose.md +2 -1
- package/commands/explain.md +2 -1
- package/commands/forge-stats.md +122 -2
- package/commands/forgedock-init.md +1 -1
- package/commands/geo-audit.md +3 -0
- package/commands/incident-response.md +2 -1
- package/commands/issue.md +24 -3
- package/commands/milestone.md +77 -12
- package/commands/optimize.md +2 -1
- package/commands/orchestrate/config.md +67 -0
- package/commands/orchestrate/phase-1-resolve.md +159 -0
- package/commands/orchestrate/phase-2-triage.md +126 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
- package/commands/orchestrate/phase-3-dependency.md +591 -0
- package/commands/orchestrate/phase-4-execution.md +962 -0
- package/commands/orchestrate/phase-5-cleanup.md +47 -0
- package/commands/orchestrate/phase-6-report.md +198 -0
- package/commands/orchestrate/safety.md +62 -0
- package/commands/orchestrate.md +25 -2027
- package/commands/pipeline-health/config.md +34 -0
- package/commands/pipeline-health/phase-1-context.md +180 -0
- package/commands/pipeline-health/phase-2-metrics.md +1729 -0
- package/commands/pipeline-health/phase-3-analyze.md +204 -0
- package/commands/pipeline-health/phase-4-proposals.md +117 -0
- package/commands/pipeline-health/phase-5-report.md +284 -0
- package/commands/pipeline-health/phase-6-summary.md +52 -0
- package/commands/pipeline-health.md +25 -2401
- package/commands/pipeline-resume.md +24 -1
- package/commands/pipeline-status.md +65 -0
- package/commands/qa-sweep.md +4 -1
- package/commands/quality-gate.md +326 -42
- package/commands/replay.md +2 -1
- package/commands/review-pr-agents/api.md +135 -0
- package/commands/review-pr-agents/auth.md +137 -0
- package/commands/review-pr-agents/billing.md +101 -0
- package/commands/review-pr-agents/concurrency.md +145 -0
- package/commands/review-pr-agents/database.md +103 -0
- package/commands/review-pr-agents/frontend.md +89 -0
- package/commands/review-pr-agents/infra.md +385 -0
- package/commands/review-pr-agents/protocols.md +185 -0
- package/commands/review-pr-agents/scraper.md +185 -0
- package/commands/review-pr-agents/security.md +406 -0
- package/commands/review-pr-agents.md +45 -1722
- package/commands/review-pr-staging.md +196 -40
- package/commands/review-pr.md +612 -56
- package/commands/rollback.md +2 -1
- package/commands/scope.md +2 -1
- package/commands/security-audit.md +2 -1
- package/commands/signal-planner.md +623 -0
- package/commands/sync-ecosystem.md +1 -0
- package/commands/test-gate.md +17 -1
- package/commands/validate.md +2 -1
- package/commands/work-on/build/architect.md +28 -0
- package/commands/work-on/build/context.md +2 -0
- package/commands/work-on/build/implement.md +19 -5
- package/commands/work-on/build/validate.md +62 -6
- package/commands/work-on/build.md +187 -30
- package/commands/work-on/close.md +132 -5
- package/commands/work-on/decompose.md +19 -1
- package/commands/work-on/investigate.md +117 -2
- package/commands/work-on/review.md +48 -4
- package/commands/work-on-monolithic.md +8 -2
- package/commands/work-on.md +247 -7
- package/docs/BRAND.md +16 -5
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +36 -1
- package/docs/CONFIG.md +44 -42
- package/docs/FORGE-PROTOCOL.md +15 -1064
- package/docs/anthropic-devrel-outreach.md +2 -2
- package/docs/articles/command-decomposition-token-savings.md +214 -0
- package/docs/articles/model-release-playbook.md +131 -0
- package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
- package/docs/claude-breakpoints.json +75 -5
- package/docs/design/abc-benchmark.md +1 -1
- package/docs/design/design-spec-schema.md +1 -1
- package/docs/design/divergent-generation.md +1 -1
- package/docs/design/eval-gate-runbook.md +178 -0
- package/docs/design/show-hn-draft.md +26 -1
- package/docs/design/token-cost-baseline-1298.md +59 -0
- package/docs/eval/README.md +117 -0
- package/docs/eval/corpus/seed-v1.json +6 -0
- package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
- package/docs/site/annotations-explained.md +24 -0
- package/docs/site/command-learning-path.md +51 -14
- package/docs/site/command-reference.md +339 -113
- package/docs/site/for-companies.md +95 -0
- package/docs/site/forge-annotation-protocol.md +12 -256
- package/docs/site/getting-started.md +58 -12
- package/docs/site/github-is-the-memory.md +113 -0
- package/docs/site/how-it-works.md +3 -13
- package/docs/site/index.md +10 -2
- package/docs/spec/eval-run-result.md +147 -0
- package/docs/spec/forge-protocol-v1.md +441 -18
- package/docs/spec/label-state-machine.md +55 -0
- package/docs/spec/review-protocol.md +158 -0
- package/docs/spec-graph-schema.md +28 -0
- package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
- package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
- package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
- package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
- package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
- package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
- package/examples/forgedock-demo/README.md +41 -22
- package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
- package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
- package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
- package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
- package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
- package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
- package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
- package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
- package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
- package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
- package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
- package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
- package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
- package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
- package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
- package/examples/forgedock-demo/scripts/smoke.js +18 -1
- package/examples/forgedock-demo/src/db.js +36 -11
- package/examples/forgedock-demo/src/routes/notes.js +106 -9
- package/examples/forgedock-demo/src/server.js +4 -1
- package/package.json +12 -4
- package/packages/protocol/LICENSE +21 -0
- package/packages/protocol/src/cli.js +135 -0
- package/packages/protocol/src/emit.js +146 -0
- package/packages/protocol/src/index.js +43 -0
- package/packages/protocol/src/parse.js +229 -0
- package/packages/protocol/src/types.js +171 -0
- package/packages/protocol/src/validate.js +122 -0
- package/scripts/bench-topology-cost.mjs +372 -0
- package/scripts/check-command-docs-drift.sh +198 -0
- package/scripts/check-protocol-restatements.sh +176 -0
- package/scripts/check-registry/README.md +72 -0
- package/scripts/check-registry/manifest.json +6 -0
- package/scripts/code-index.sh +454 -0
- package/scripts/conformance-check.mjs +168 -0
- package/scripts/doctor-pipeline-state.sh +484 -0
- package/scripts/eval-gate-baseline.json +17 -0
- package/scripts/eval-gate-scorecard.mjs +242 -0
- package/scripts/eval-scorecard-aggregator.mjs +252 -0
- package/scripts/flaky-quarantine.sh +241 -0
- package/scripts/forge-annotation.sh +567 -0
- package/scripts/issue-dedup.sh +236 -0
- package/scripts/validate-annotation-node.mjs +233 -0
- package/scripts/verify-env-vars.sh +18 -2
- package/scripts/verify-host-headers.sh +4 -4
- package/scripts/verify-route-registration.sh +2 -2
- package/scripts/worktree-lifecycle.sh +202 -0
- package/templates/devdocs/agent/using-forgedock.md +14 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
---
|
|
2
|
+
install: internal
|
|
3
|
+
---
|
|
4
|
+
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
5
|
+
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
6
|
+
|
|
7
|
+
# /orchestrate — Phase 3: Dependency Analysis & Execution Plan
|
|
8
|
+
|
|
9
|
+
## Phase 3: Dependency Analysis & Execution Plan
|
|
10
|
+
|
|
11
|
+
### Step 3A: Analyze explicit dependencies
|
|
12
|
+
|
|
13
|
+
For each issue (including newly spawned ones from Phase 2), check:
|
|
14
|
+
1. **Explicit dependencies**: Issue body contains "Depends on #X" or "Blocked by #X"
|
|
15
|
+
2. **Milestone ordering**: Issues within a milestone may have a natural order (foundation → features → UI)
|
|
16
|
+
3. **File conflicts**: If two issues modify the same files, they should be sequential (not parallel)
|
|
17
|
+
4. **Parent-child links**: If issue body contains "Parent investigation: #{X}", it was spawned by an investigation — no special ordering needed unless explicitly stated
|
|
18
|
+
|
|
19
|
+
Read each issue's body briefly to check for dependency markers:
|
|
20
|
+
```bash
|
|
21
|
+
for NUM in {issue_numbers}; do
|
|
22
|
+
gh issue view $NUM --json body --jq '.body' | grep -iE 'depends on|blocked by|after #|parent investigation' || echo "no deps"
|
|
23
|
+
done
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Step 3B: Domain estimation
|
|
27
|
+
|
|
28
|
+
For each issue, estimate which domains it touches based on title, body, and labels. This improves DAG construction — issues in the same domain likely touch the same files and should be serialized (one becomes a predecessor of the other).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
for NUM in {issue_numbers}; do
|
|
32
|
+
ISSUE=$(gh issue view $NUM --json title,body,labels --jq '{title: .title, labels: [.labels[].name], body: (.body[:300])}')
|
|
33
|
+
echo "=== #$NUM ==="
|
|
34
|
+
echo "$ISSUE" | grep -qiE "credit|billing|pricing|stripe|charge|refund" && echo " BILLING" || true
|
|
35
|
+
echo "$ISSUE" | grep -qiE "auth|session|jwt|login|permission|oauth" && echo " AUTH" || true
|
|
36
|
+
echo "$ISSUE" | grep -qiE "worker|queue|job|task|background|consumer" && echo " WORKER" || true
|
|
37
|
+
echo "$ISSUE" | grep -qiE "migration|\.sql|database|postgres|alembic" && echo " DATABASE" || true
|
|
38
|
+
echo "$ISSUE" | grep -qiE "component|page|layout|dashboard|ui|ux|frontend|web/src" && echo " FRONTEND" || true
|
|
39
|
+
echo "$ISSUE" | grep -qiE "docker|deploy|traefik|nginx|ci|cd|infra|github.action" && echo " INFRA" || true
|
|
40
|
+
echo "$ISSUE" | grep -qiE "llm|extract|schema|format|embedding|model" && echo " AI" || true
|
|
41
|
+
# For project-specific domains, configure keywords in forge.yaml → review.domains and extend above
|
|
42
|
+
done
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Use domain info for DAG edge construction:**
|
|
46
|
+
- Issues in the SAME domain (especially WORKER, BILLING, DATABASE) are more likely to touch the same files → add predecessor edges to serialize them
|
|
47
|
+
- Issues in DIFFERENT domains are more likely independent → safe to parallelize
|
|
48
|
+
- BILLING + AUTH issues should be prioritized early (security-critical)
|
|
49
|
+
- **DATABASE issues are ALWAYS serialized — hard rule, no exceptions.** Multiple agents writing migrations simultaneously will produce duplicate migration numbers (e.g., two `0067_*.sql` files), which breaks the migration runner. DATABASE issues form a linear predecessor chain in the DAG. If 3 DATABASE issues are in a batch: A has no predecessors, B has {A} as predecessor, C has {B} as predecessor.
|
|
50
|
+
|
|
51
|
+
**Store domain tags per issue** for use in the plan presentation (Step 3E).
|
|
52
|
+
|
|
53
|
+
### Step 3C: Multi-layer conflict detection
|
|
54
|
+
|
|
55
|
+
Domain estimation (above) catches broad category overlap but misses cases where two issues modify the exact same file without mentioning the same keywords, or where two issues touch different files that share indirect dependencies (imports, config, barrel exports). This step uses three layers of structural analysis to catch conflicts the keyword heuristic misses.
|
|
56
|
+
|
|
57
|
+
#### Layer 1: Explicit file-overlap extraction
|
|
58
|
+
|
|
59
|
+
**For issues that already have an INVESTIGATOR comment** (from Wave 0 or a prior session), extract their Affected Files list. **For issues WITHOUT an investigation comment**, fall back to parsing the issue body for file paths. Both code paths accumulate into a single `LAYER1_FILES` array (declared once, before the loop) — this is the batch-wide file set that Layer 5's co-change query (below) reuses, per the "file list already extracted in Layer 1" reference in Layer 2 and Layer 5:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
LAYER1_FILES=()
|
|
63
|
+
for NUM in {issue_numbers}; do
|
|
64
|
+
echo "=== #$NUM ==="
|
|
65
|
+
FILES_FOR_NUM=$(gh api repos/{GH_REPO}/issues/${NUM}/comments \
|
|
66
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body' 2>/dev/null \
|
|
67
|
+
| grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | tr -d '`' | sort -u)
|
|
68
|
+
|
|
69
|
+
# Fall back to parsing the issue body directly when no INVESTIGATOR comment exists yet.
|
|
70
|
+
if [ -z "$FILES_FOR_NUM" ]; then
|
|
71
|
+
FILES_FOR_NUM=$(gh issue view $NUM --json body --jq '.body' \
|
|
72
|
+
| grep -oP '`[^`]*\.(py|tsx?|jsx?|sql|json|ya?ml)`' | tr -d '`' | sort -u)
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
echo "$FILES_FOR_NUM"
|
|
76
|
+
|
|
77
|
+
# Accumulate into the batch-wide array — read line-by-line so each extracted path
|
|
78
|
+
# becomes one array element (paths here don't contain spaces, but this stays robust).
|
|
79
|
+
while IFS= read -r f; do
|
|
80
|
+
[ -n "$f" ] && LAYER1_FILES+=("$f")
|
|
81
|
+
done <<< "$FILES_FOR_NUM"
|
|
82
|
+
done
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Cross-reference all extracted file lists:**
|
|
86
|
+
- If two issues share ANY affected file → one MUST be a predecessor of the other (serialized)
|
|
87
|
+
- The issue with lower issue number goes first (stable ordering), unless an explicit `Depends on #` says otherwise
|
|
88
|
+
- Add a conflict note to the DAG plan: "#{A} and #{B} both modify `{file}` — #{A} is predecessor of #{B}"
|
|
89
|
+
|
|
90
|
+
#### Layer 2: Directory-proximity detection
|
|
91
|
+
|
|
92
|
+
Two issues that modify different files in the **same leaf directory** have a high probability of conflicting through shared `__init__.py`, `index.ts`, barrel re-exports, or tightly coupled sibling modules. This layer catches indirect conflicts that Layer 1 misses.
|
|
93
|
+
|
|
94
|
+
**Extract the leaf directory for each affected file:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# For each issue's file list, extract unique directories
|
|
98
|
+
for NUM in {issue_numbers}; do
|
|
99
|
+
echo "=== #$NUM directories ==="
|
|
100
|
+
# From the file list already extracted in Layer 1:
|
|
101
|
+
echo "$FILES_FOR_NUM" | xargs -I{} dirname {} | sort -u
|
|
102
|
+
done
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Cross-reference directory lists:**
|
|
106
|
+
- If two issues share a leaf directory AND that directory is "small" (contains fewer than 10 tracked files), flag as **probable conflict** → serialize
|
|
107
|
+
- If the shared directory is a broad container (e.g., `services/api/app/routers/` with 15+ files), downgrade to **possible conflict** → serialize only if same domain tag (from Step 3B)
|
|
108
|
+
- **Known high-conflict directories** (always serialize if shared):
|
|
109
|
+
- `services/api/app/models/` (SQLAlchemy models often share Base, imports)
|
|
110
|
+
- `services/api/app/core/` (shared dependencies)
|
|
111
|
+
- `services/worker/worker/` (tightly coupled consumer modules)
|
|
112
|
+
- `web/src/lib/` (shared utilities)
|
|
113
|
+
- `shared/` (volume-mounted, affects all services)
|
|
114
|
+
- `infra/migrations/` (already covered by DATABASE hard rule, but explicit here too)
|
|
115
|
+
|
|
116
|
+
#### Layer 3: Shared-module inference
|
|
117
|
+
|
|
118
|
+
When two issues modify different files that **import from the same utility/init module**, both agents often end up modifying that shared module (adding imports, updating re-exports). This creates merge conflicts invisible to Layer 1.
|
|
119
|
+
|
|
120
|
+
**Heuristic rules (no git operations needed — pattern-based):**
|
|
121
|
+
|
|
122
|
+
| Pattern | Inference | Action |
|
|
123
|
+
|---------|-----------|--------|
|
|
124
|
+
| Issue A modifies `routers/billing.py`, Issue B modifies `routers/auth.py` | Both likely import from `routers/__init__.py` or `dependencies.py` | Flag as possible conflict if same service |
|
|
125
|
+
| Issue A modifies `models/user.py`, Issue B modifies `models/subscription.py` *(Python/SQLAlchemy example)* | Both likely modify `models/__init__.py` (model registry) | Flag as probable conflict → serialize |
|
|
126
|
+
| Issue A modifies `routes/users.js`, Issue B modifies `routes/billing.js` *(Node.js example)* | Both likely import from `routes/index.js` barrel | Flag as possible conflict if same service |
|
|
127
|
+
| Issue A modifies `internal/user/handler.go`, Issue B modifies `internal/billing/handler.go` *(Go example)* | Both likely register in `internal/router/router.go` | Flag as possible conflict if same service |
|
|
128
|
+
| Issue A modifies `web/src/components/X.tsx`, Issue B modifies `web/src/components/Y.tsx` | May share `index.ts` barrel export | Flag only if same parent directory |
|
|
129
|
+
| Issue A modifies the app entrypoint (e.g. `main.py`, `main.go`, `server.js`, `app.ts`) | Entrypoint is a high-fan-in file (router registration, middleware) — set the path via `forge.yaml → review.layout.api_main` | Serialize with ANY other same-service issue |
|
|
130
|
+
| Issue A modifies `docker-compose.yml` or `docker-compose.prod.yml` | Global config — any concurrent modification conflicts | Serialize with ALL other issues that touch infra |
|
|
131
|
+
|
|
132
|
+
**Apply inferences:**
|
|
133
|
+
```
|
|
134
|
+
# High-fan-in files — if ANY issue touches these, serialize it with all same-service issues.
|
|
135
|
+
# Read layout paths from forge.yaml review.layout; fall back to sensible generic defaults.
|
|
136
|
+
# Example (pseudo-code — adapt to your forge.yaml parsing method):
|
|
137
|
+
# API_MAIN = forge_yaml.review.layout.api_main ?? "src/main.py" # set in forge.yaml; no stack-specific default
|
|
138
|
+
# WORKER_MAIN = forge_yaml.review.layout.worker_main ?? "src/worker.py" # set in forge.yaml; no stack-specific default
|
|
139
|
+
# PAGES_ROOT = forge_yaml.review.layout.pages ?? "web/src/app" # Next.js default; override in forge.yaml for other frameworks
|
|
140
|
+
|
|
141
|
+
HIGH_FAN_IN = [
|
|
142
|
+
API_MAIN, # app entrypoint — router/middleware registration (set forge.yaml review.layout.api_main)
|
|
143
|
+
WORKER_MAIN, # worker entrypoint (set forge.yaml review.layout.worker_main)
|
|
144
|
+
PAGES_ROOT + "/layout.tsx", # root layout for all pages (Next.js; adapt for your framework)
|
|
145
|
+
"docker-compose.yml",
|
|
146
|
+
"docker-compose.prod.yml",
|
|
147
|
+
".env.example"
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# For each issue, check if affected files include a high-fan-in file
|
|
151
|
+
# If yes: that issue cannot be parallelized with any other issue touching the same service
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### Layer 4: Conservative fallback (low-confidence cases)
|
|
155
|
+
|
|
156
|
+
When file extraction yields **fewer than 2 file paths** for an issue (common for issues without investigation comments, or issues described in prose without backtick-wrapped paths), the conflict detection has low confidence. Rather than assuming independence, apply conservative serialization.
|
|
157
|
+
|
|
158
|
+
**Rules:**
|
|
159
|
+
- If an issue has 0-1 extracted file paths AND shares a domain tag (from Step 3B) with another issue → serialize them
|
|
160
|
+
- If an issue has 0 extracted file paths AND no domain tag could be determined → add it as a predecessor of the next same-domain issue, or if no domain match exists, serialize it after the most recently added issue (safest default)
|
|
161
|
+
- Add a note to the plan: "#{N} — low file-extraction confidence, serialized conservatively"
|
|
162
|
+
|
|
163
|
+
**Rationale**: The cost of a false-negative (two agents conflict → one fails with merge error, wasting the full agent run) far exceeds the cost of a false-positive (an issue waits for one predecessor before starting). Always err toward serialization when uncertain.
|
|
164
|
+
|
|
165
|
+
#### Layer 5: Historical co-change coupling <!-- Added: forge#1196 --> <!-- Empty-set guard: forge#1206 -->
|
|
166
|
+
|
|
167
|
+
Layers 1-4 infer conflict risk from structure — path overlap, directory nesting, hard-coded high-fan-in lists. They miss the case where two files with no directory or naming relationship have historically changed together in the same commits (e.g. `models/user.py` and `services/billing/charge.py`), and they over-serialize the inverse case where files merely sit near each other but have never actually co-changed. Git commit history answers both questions directly and empirically. This layer reads **commit metadata only** — the list of files touched per commit — never file contents, so it does not violate Hard Rule 2 ("dispatcher, not a builder... never read code"). This is the same category of operation already established as compliant in `commands/work-on/investigate.md` and `commands/work-on/build/context.md`, which mine `git log` for issue cross-referencing.
|
|
168
|
+
|
|
169
|
+
**Bounded query** — restrict both the time window and the file set so this stays a small, deterministic, single-shot lookup (never a full-repo co-change matrix). An empty `ALL_AFFECTED_FILES` set MUST short-circuit before the query runs — passing an empty array to `git log --` does not mean "match nothing", it means "no pathspec restriction", which would silently widen the query to the entire repo:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Union of affected files across all issues in the CURRENT batch only (already
|
|
173
|
+
# extracted per-issue in Layer 1) — never the whole repo. Built as an array
|
|
174
|
+
# (not a newline-joined scalar) so each path survives as a single pathspec
|
|
175
|
+
# argument below — a plain string here would be word-split and glob-expanded
|
|
176
|
+
# by the shell when handed to `git log --`, silently mangling or dropping any
|
|
177
|
+
# path containing a space or glob metacharacter.
|
|
178
|
+
mapfile -t ALL_AFFECTED_FILES < <(printf '%s\n' "${LAYER1_FILES[@]}" | sort -u | grep -v '^$')
|
|
179
|
+
|
|
180
|
+
# Guard: an empty array expands to nothing after `--`, which git interprets as
|
|
181
|
+
# "no pathspec restriction" (i.e. the whole repo) rather than "match nothing".
|
|
182
|
+
# Skip the query entirely in that case instead of letting it silently widen to
|
|
183
|
+
# a full-repo scan — this can happen when upstream file-extraction (Layer 1)
|
|
184
|
+
# yields zero paths for every issue in the batch.
|
|
185
|
+
# NOTE: `grep -v '^$'` above is required — without it, `printf '%s\n'` on an
|
|
186
|
+
# empty array emits one blank line (printf runs its format string once even
|
|
187
|
+
# with zero variadic args), which `mapfile` captures as a 1-element array of
|
|
188
|
+
# [""], making this guard evaluate to false and causing `git log -- ""` to run.
|
|
189
|
+
if [ "${#ALL_AFFECTED_FILES[@]}" -eq 0 ]; then
|
|
190
|
+
echo "Layer 5: no affected files extracted by Layer 1 for this batch — skipping co-change query, falling back to Layers 1-4."
|
|
191
|
+
else
|
|
192
|
+
# Bounded window: last 90 days, capped at 200 commits — whichever is smaller.
|
|
193
|
+
# Each commit's file list is delimited by a marker so co-occurring files can be
|
|
194
|
+
# grouped per-commit in a single pass. The array is expanded quoted
|
|
195
|
+
# ("${ALL_AFFECTED_FILES[@]}") so every path is passed as one literal argument.
|
|
196
|
+
git log --name-only --since="90 days ago" --max-count=200 \
|
|
197
|
+
--pretty=format:'---%H---' -- "${ALL_AFFECTED_FILES[@]}" \
|
|
198
|
+
> /tmp/cochange_log.txt
|
|
199
|
+
|
|
200
|
+
# Parse into commit → file-set groups, then increment a co-occurrence counter
|
|
201
|
+
# for every unordered pair of files that appear in the SAME commit's file list.
|
|
202
|
+
# (Illustrative — an agent executing this reads /tmp/cochange_log.txt and tallies
|
|
203
|
+
# pairs; no separate script is shipped, matching the pseudo-code style of Layers 1-4.)
|
|
204
|
+
fi
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Scoring rule**: A file pair is **co-change coupled** when it appears together in **3 or more** of the commits captured by the bounded query above. A pair with **zero** co-occurrences across the entire window is **verified independent**.
|
|
208
|
+
|
|
209
|
+
**Apply the signal:**
|
|
210
|
+
- **High co-change pair spans two different issues in the batch** → add a serialization edge between them (same directed-edge convention as Layers 1-4: lower issue number is predecessor), OR, if the pair also carries competing investigation recommendations, flag it for Phase 2.5 arbitration instead of a blind serialization edge (see cross-reference in Step 2.5B below).
|
|
211
|
+
- **Verified-independent pair** → MAY be used to downgrade an existing Layer 2 "broad directory + different domain" or Layer 4 "conservative fallback" serialization to parallel. This downgrade is **never** applied to Layer 1 (same-file hard conflict, which is ground truth from the current batch, not historical inference) or to Layer 3 high-fan-in-file edges (a file can be structurally high-risk even with a thin history window, e.g. a newly added `main.py`).
|
|
212
|
+
- If `ALL_AFFECTED_FILES` is empty, the guard above skips the query and Layer 5 contributes nothing for the entire batch. If the bounded query runs but returns no commits (e.g. brand-new files, or window/pair-set too small to have history) → Layer 5 contributes nothing; fall back silently to Layers 1-4's existing verdict for that pair.
|
|
213
|
+
|
|
214
|
+
**Rationale**: Empirical co-change is a strictly stronger signal than directory proximity or naming convention — it is the actual observed outcome the other layers are trying to approximate. Bounding the window and pair-set keeps the query cheap and deterministic while still catching the cross-directory conflicts Layers 1-4 structurally cannot see.
|
|
215
|
+
|
|
216
|
+
#### Combining all layers
|
|
217
|
+
|
|
218
|
+
Build the final conflict graph by merging signals from all five layers:
|
|
219
|
+
|
|
220
|
+
| Signal | Strength | Action |
|
|
221
|
+
|--------|----------|--------|
|
|
222
|
+
| Layer 1: Same file | **Hard conflict** | Always serialize |
|
|
223
|
+
| Layer 2: Same small directory | **Probable conflict** | Serialize |
|
|
224
|
+
| Layer 2: Same broad directory + same domain | **Probable conflict** | Serialize |
|
|
225
|
+
| Layer 2: Same broad directory + different domain | **Possible conflict** | Parallelize (accept risk) — unless Layer 5 shows high co-change, then serialize |
|
|
226
|
+
| Layer 3: High-fan-in file touched | **Probable conflict** | Serialize with same-service issues |
|
|
227
|
+
| Layer 3: Shared model/init pattern | **Probable conflict** | Serialize |
|
|
228
|
+
| Layer 4: Low confidence + same domain | **Conservative** | Serialize |
|
|
229
|
+
| Layer 4: Low confidence + no domain | **Conservative** | Add predecessor edge to most recent issue — unless Layer 5 shows verified independence, then parallelize |
|
|
230
|
+
| Layer 5: High co-change (3+ shared commits, cross-issue file pair) | **Probable conflict** | Serialize (or route to Phase 2.5 arbitration if a competing-recommendation conflict is also present) |
|
|
231
|
+
| Layer 5: Verified independent (zero shared commits) | **Downgrade signal** | Permits downgrading a Layer 2 "broad directory + different domain" or Layer 4 verdict to parallel — never overrides Layer 1 or Layer 3 |
|
|
232
|
+
|
|
233
|
+
**This supplements, not replaces, the domain keyword estimation.** Domain tags still help with broad sequencing decisions. Multi-layer conflict detection catches the specific cases keywords miss (e.g., two issues that both modify files in `services/api/app/models/` where one is labeled WORKER and the other BILLING — Layer 2 catches this even though Layer 1 shows no direct file overlap; or two issues touching files in unrelated directories that Layer 5 shows have co-changed in 4 of the last 12 commits touching either file).
|
|
234
|
+
|
|
235
|
+
### Step 3D: Build the dependency DAG
|
|
236
|
+
|
|
237
|
+
Build a **directed acyclic graph (DAG)** of per-issue dependencies. Each issue gets a `predecessors` set — the specific issues that must reach a terminal state before this issue can dispatch. This replaces the previous wave-grouping model where all issues in a wave had to complete before any issue in the next wave could start.
|
|
238
|
+
|
|
239
|
+
**DAG construction rules:**
|
|
240
|
+
- Investigation issues already ran in Phase 2 — they are NOT included in this DAG
|
|
241
|
+
- Each issue starts with an empty predecessor set
|
|
242
|
+
- **Explicit dependencies**: If issue B says "Depends on #A" or "Blocked by #A", add A to B's predecessors
|
|
243
|
+
- **File-conflict edges**: If two issues share affected files (from Step 3C Layer 1), add a directed edge: lower issue number → higher issue number (unless explicit deps say otherwise). The later issue has the earlier issue in its predecessors.
|
|
244
|
+
- **Domain serialization edges**: DATABASE issues form a linear chain (each has the previous DATABASE issue as its predecessor). Same-small-directory issues (Layer 2) and high-fan-in file issues (Layer 3) get directed edges as per Step 3C rules.
|
|
245
|
+
- **Conservative fallback edges**: Low-confidence issues (Layer 4) get edges to same-domain issues as per Step 3C rules.
|
|
246
|
+
- **Co-change coupling edges** <!-- Added: forge#1196 -->: High co-change file pairs (Layer 5, 3+ shared commits in the bounded window) that span two different issues get a directed edge using the same lower-issue-number-is-predecessor convention as Layer 1. Verified-independent pairs (Layer 5, zero shared commits) may instead REMOVE an edge that Layer 2 or Layer 4 would otherwise have added for that pair — Layer 1 and Layer 3 edges are never removed by a Layer 5 downgrade.
|
|
247
|
+
- **No artificial concurrency limit by default** — all issues with empty predecessor sets dispatch simultaneously. The only constraints are file overlap, explicit dependencies, and co-change coupling. When `forge.yaml → orchestration.max_concurrent` is set, the dispatch loop queues excess ready issues and releases them as running workers complete (see Engine mode § Concurrency model).
|
|
248
|
+
|
|
249
|
+
**Terminology:**
|
|
250
|
+
- **Ready issues**: Issues whose predecessor set is empty (all predecessors have reached terminal state or were never added)
|
|
251
|
+
- **Blocked issues**: Issues with one or more unresolved predecessors
|
|
252
|
+
- **Critical path**: The longest chain of dependent issues in the DAG — determines minimum wall-clock time
|
|
253
|
+
|
|
254
|
+
**Example DAG:**
|
|
255
|
+
```
|
|
256
|
+
Phase 2 (already done): #2644 (investigation) → spawned #2645, #2646, #2647
|
|
257
|
+
|
|
258
|
+
Dependency graph:
|
|
259
|
+
#2633 (orphaned queues) → predecessors: {} ← READY
|
|
260
|
+
#2636 (invalidation sub) → predecessors: {} ← READY
|
|
261
|
+
#2645 (new finding) → predecessors: {} ← READY
|
|
262
|
+
#2646 (new finding) → predecessors: {} ← READY
|
|
263
|
+
#2634 (enable daemon) → predecessors: {#2633} ← blocked until #2633 completes
|
|
264
|
+
#2647 (depends on #2645) → predecessors: {#2645} ← blocked until #2645 completes
|
|
265
|
+
|
|
266
|
+
Critical path: #2633 → #2634 (2 steps) or #2645 → #2647 (2 steps)
|
|
267
|
+
Initial dispatch: #2633, #2636, #2645, #2646 (all ready — launched simultaneously)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Key advantage over waves**: When #2633 completes, #2634 dispatches immediately — it does not wait for #2636, #2645, or #2646 to finish. Similarly, when #2645 completes, #2647 dispatches immediately regardless of other issues' status.
|
|
271
|
+
|
|
272
|
+
### Step 3D.5: Cycle Detection (MANDATORY) <!-- Added: forge#1085 -->
|
|
273
|
+
|
|
274
|
+
**Run immediately after Step 3D's DAG edge construction, before presenting the plan (Step 3E).** This step validates that the predecessor graph is acyclic. Without it, mutual `Depends on` declarations (e.g., A depends on B AND B depends on A) cause both issues to remain permanently blocked in Step 4B's dispatch loop — no error, no timeout, indefinite deadlock.
|
|
275
|
+
|
|
276
|
+
**Algorithm**: Kahn's topological sort. Runs in O(V+E) — negligible overhead for typical batch sizes.
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# --- Step 3D.5: Cycle Detection ---
|
|
280
|
+
# Inputs:
|
|
281
|
+
# ISSUES[] — array of all issue numbers in the DAG
|
|
282
|
+
# PREDECESSORS[N] — space-separated list of predecessor issue numbers for issue N
|
|
283
|
+
|
|
284
|
+
# Step 1: Compute in-degree for each issue
|
|
285
|
+
declare -A IN_DEGREE
|
|
286
|
+
for NUM in "${ISSUES[@]}"; do
|
|
287
|
+
IN_DEGREE[$NUM]=0
|
|
288
|
+
done
|
|
289
|
+
for NUM in "${ISSUES[@]}"; do
|
|
290
|
+
for PRED in ${PREDECESSORS[$NUM]:-}; do
|
|
291
|
+
IN_DEGREE[$NUM]=$(( ${IN_DEGREE[$NUM]:-0} + 1 ))
|
|
292
|
+
done
|
|
293
|
+
done
|
|
294
|
+
|
|
295
|
+
# Step 2: Seed the queue with zero-in-degree issues (no predecessors)
|
|
296
|
+
KAHN_QUEUE=()
|
|
297
|
+
for NUM in "${ISSUES[@]}"; do
|
|
298
|
+
[ "${IN_DEGREE[$NUM]}" -eq 0 ] && KAHN_QUEUE+=("$NUM")
|
|
299
|
+
done
|
|
300
|
+
|
|
301
|
+
# Step 3: Process queue — reduce successor in-degrees, enqueue newly freed issues
|
|
302
|
+
PROCESSED_COUNT=0
|
|
303
|
+
PROCESSED_ORDER=()
|
|
304
|
+
while [ "${#KAHN_QUEUE[@]}" -gt 0 ]; do
|
|
305
|
+
# Dequeue
|
|
306
|
+
CURRENT="${KAHN_QUEUE[0]}"
|
|
307
|
+
KAHN_QUEUE=("${KAHN_QUEUE[@]:1}")
|
|
308
|
+
PROCESSED_ORDER+=("$CURRENT")
|
|
309
|
+
PROCESSED_COUNT=$(( PROCESSED_COUNT + 1 ))
|
|
310
|
+
|
|
311
|
+
# Reduce in-degree of all issues that depend on CURRENT (i.e., CURRENT is in their PREDECESSORS)
|
|
312
|
+
for SUCCESSOR in "${ISSUES[@]}"; do
|
|
313
|
+
for PRED in ${PREDECESSORS[$SUCCESSOR]:-}; do
|
|
314
|
+
if [ "$PRED" = "$CURRENT" ]; then
|
|
315
|
+
IN_DEGREE[$SUCCESSOR]=$(( ${IN_DEGREE[$SUCCESSOR]} - 1 ))
|
|
316
|
+
[ "${IN_DEGREE[$SUCCESSOR]}" -eq 0 ] && KAHN_QUEUE+=("$SUCCESSOR")
|
|
317
|
+
fi
|
|
318
|
+
done
|
|
319
|
+
done
|
|
320
|
+
done
|
|
321
|
+
|
|
322
|
+
# Step 4: Any issue not processed has in-degree > 0 — part of a cycle
|
|
323
|
+
CYCLE_ISSUES=()
|
|
324
|
+
EXCLUDED_CYCLE=()
|
|
325
|
+
for NUM in "${ISSUES[@]}"; do
|
|
326
|
+
FOUND=false
|
|
327
|
+
for P in "${PROCESSED_ORDER[@]}"; do [ "$P" = "$NUM" ] && FOUND=true && break; done
|
|
328
|
+
[ "$FOUND" = "false" ] && CYCLE_ISSUES+=("$NUM")
|
|
329
|
+
done
|
|
330
|
+
|
|
331
|
+
# Step 5: Handle cycles
|
|
332
|
+
if [ "${#CYCLE_ISSUES[@]}" -gt 0 ]; then
|
|
333
|
+
echo "CYCLE DETECTED in dependency graph — the following issues form a circular dependency:"
|
|
334
|
+
for C in "${CYCLE_ISSUES[@]}"; do
|
|
335
|
+
echo " #${C}: predecessors=[${PREDECESSORS[$C]}]"
|
|
336
|
+
# Label each cyclic issue needs-human
|
|
337
|
+
gh issue edit "$C" -R {GH_REPO} --add-label "needs-human" 2>/dev/null || true
|
|
338
|
+
gh issue comment "$C" -R {GH_REPO} --body "**Cycle detected by /orchestrate**: This issue is part of a circular dependency chain involving issues: ${CYCLE_ISSUES[*]/#/#}. The orchestrator cannot dispatch it automatically. Please fix the \`Depends on\` declarations so that no cycle exists, then re-run /orchestrate." 2>/dev/null || true
|
|
339
|
+
# Remove from DAG — store in EXCLUDED_CYCLE for Step 3E reporting
|
|
340
|
+
EXCLUDED_CYCLE+=("$C")
|
|
341
|
+
# Remove from ISSUES array for all downstream processing
|
|
342
|
+
# Use exact-match filter loop — pattern substitution (${array[@]/pattern}) leaves blank
|
|
343
|
+
# slots and corrupts partial matches (e.g., removing 100 changes 1000 to 0).
|
|
344
|
+
NEW_ISSUES=()
|
|
345
|
+
for I in "${ISSUES[@]}"; do
|
|
346
|
+
[ "$I" != "$C" ] && NEW_ISSUES+=("$I")
|
|
347
|
+
done
|
|
348
|
+
ISSUES=("${NEW_ISSUES[@]}")
|
|
349
|
+
done
|
|
350
|
+
echo ""
|
|
351
|
+
echo "These issues have been labeled needs-human and excluded from the DAG."
|
|
352
|
+
echo "Fix their dependency declarations and re-run /orchestrate."
|
|
353
|
+
else
|
|
354
|
+
echo "DAG cycle check: PASS — no cycles detected. Proceeding with ${#PROCESSED_ORDER[@]} issues."
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
# Guard: if all issues were cyclic, ISSUES[] is now empty — abort before presenting an empty plan
|
|
358
|
+
if [ "${#ISSUES[@]}" -eq 0 ]; then
|
|
359
|
+
echo ""
|
|
360
|
+
echo "ERROR: All issues in this batch form circular dependencies and have been excluded."
|
|
361
|
+
echo "Every issue has been labeled needs-human."
|
|
362
|
+
echo "Fix the Depends on / Blocked by declarations so no cycle exists, then re-run /orchestrate."
|
|
363
|
+
exit 1
|
|
364
|
+
fi
|
|
365
|
+
# --- End Step 3D.5 ---
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**After this step**:
|
|
369
|
+
- `ISSUES[]` contains only acyclic issues — safe to dispatch
|
|
370
|
+
- `EXCLUDED_CYCLE[]` contains cyclic issue numbers — reported in Step 3E, never dispatched
|
|
371
|
+
- If `EXCLUDED_CYCLE` is non-empty, report it clearly in the Step 3E plan before asking for user confirmation
|
|
372
|
+
- If `ISSUES[]` is empty after cycle exclusion (all issues were cyclic), the guard above aborts with `exit 1` — Step 3E is never reached with an empty plan <!-- Added: forge#1110 -->
|
|
373
|
+
|
|
374
|
+
### Step 3E: Present the plan to the user
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
## Orchestration Plan
|
|
378
|
+
|
|
379
|
+
**Scope**: {milestone name / "N issues" / "fast-lane"}
|
|
380
|
+
**Total issues**: {count} ({investigation_count} investigations + {implementation_count} implementations)
|
|
381
|
+
**Execution model**: Dependency-graph streaming (issues dispatch as predecessors complete)
|
|
382
|
+
|
|
383
|
+
{IF investigations exist:}
|
|
384
|
+
### Investigations (run first, may spawn new issues)
|
|
385
|
+
| # | Title | Expected Output |
|
|
386
|
+
|---|-------|----------------|
|
|
387
|
+
| #{INV1} | {title} | New issues → folded into dependency graph |
|
|
388
|
+
|
|
389
|
+
### Implementation (after investigations complete)
|
|
390
|
+
{END IF}
|
|
391
|
+
|
|
392
|
+
### Domain Distribution
|
|
393
|
+
| Domain | Issues | Notes |
|
|
394
|
+
|--------|--------|-------|
|
|
395
|
+
| FRONTEND | {N} | {Independent pages / Shared components} |
|
|
396
|
+
| BILLING | {N} | {Critical — dispatches immediately} |
|
|
397
|
+
| DATABASE | {N} | {Serialized chain — migration order matters} |
|
|
398
|
+
| AUTH | {N} | {Critical — dispatches immediately} |
|
|
399
|
+
| WORKER | {N} | {High overlap risk within worker service} |
|
|
400
|
+
| AI | {N} | {Independent} |
|
|
401
|
+
| INFRA | {N} | {Independent} |
|
|
402
|
+
|
|
403
|
+
(Omit rows with 0 issues. Add project-specific domain rows from forge.yaml → review.domains.)
|
|
404
|
+
|
|
405
|
+
### Dependency Graph
|
|
406
|
+
|
|
407
|
+
| Issue | Predecessors | Domain | Status |
|
|
408
|
+
|-------|-------------|--------|--------|
|
|
409
|
+
| #{A} | — | FRONTEND | Ready (dispatches immediately) |
|
|
410
|
+
| #{B} | — | BILLING | Ready (dispatches immediately) |
|
|
411
|
+
| #{C} | — | WORKER | Ready (dispatches immediately) |
|
|
412
|
+
| #{D} | #{A} | FRONTEND | Blocked (waits for #{A} only) |
|
|
413
|
+
| #{E} | — | DATABASE | Ready (dispatches immediately) |
|
|
414
|
+
| #{F} | #{E} | DATABASE | Blocked (serialized — waits for #{E}) |
|
|
415
|
+
|
|
416
|
+
**Critical path**: #{E} → #{F} (2 steps, determines minimum wall-clock time)
|
|
417
|
+
**Initial dispatch**: #{A}, #{B}, #{C}, #{E} (all predecessors resolved)
|
|
418
|
+
**Streaming**: #{D} dispatches as soon as #{A} completes — does NOT wait for #{B}, #{C}, or #{E}
|
|
419
|
+
|
|
420
|
+
**Note**: Investigations may create additional issues that will be automatically added to the dependency graph. The final graph will be confirmed after investigations complete.
|
|
421
|
+
|
|
422
|
+
**Excluded** (already in progress / ineligible):
|
|
423
|
+
- #{X} — {reason}
|
|
424
|
+
|
|
425
|
+
{IF EXCLUDED_CYCLE is non-empty:}
|
|
426
|
+
### ⚠ Circular Dependencies Detected — Manual Fix Required
|
|
427
|
+
|
|
428
|
+
The following issues form a circular dependency chain and **cannot be dispatched** until the cycle is resolved:
|
|
429
|
+
|
|
430
|
+
| Issue | Depends On | Problem |
|
|
431
|
+
|-------|------------|---------|
|
|
432
|
+
{rows: each EXCLUDED_CYCLE issue, its predecessor list, "mutual dependency — forms cycle with #{other}"}
|
|
433
|
+
|
|
434
|
+
**Action required**: Edit each issue's body to remove or correct the `Depends on` / `Blocked by` declarations so no cycle exists. Each issue has been labeled `needs-human`. After fixing, re-run `/orchestrate` to dispatch them.
|
|
435
|
+
{END IF}
|
|
436
|
+
|
|
437
|
+
Proceed? (yes / adjust / pick specific issues)
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
**Wait for user confirmation before spawning agents.** This is the checkpoint — once agents launch, they run autonomously.
|
|
441
|
+
|
|
442
|
+
**After confirmation**: If investigations exist, execute Phase 2B-E first. Then re-present the expanded plan (with newly spawned issues added to the dependency graph) for a quick confirmation before launching implementation.
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
## Engine mode (headless)
|
|
447
|
+
|
|
448
|
+
When running headless/CI, dispatch each issue via the durable execution engine instead of spawning prose agents:
|
|
449
|
+
|
|
450
|
+
```bash
|
|
451
|
+
forgedock run-issue <issue> --lane <staging|milestone/slug>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
The engine drives every phase transition deterministically, mirrors state to the `FORGE:STATE` block on the issue, and holds a lease. To recover stalls, scan in-flight issues' `FORGE:STATE`; any issue with an expired lease and a non-terminal state is re-dispatched with the same `forgedock run-issue <issue>` command — it resumes from the last committed phase (idempotent). This replaces the label-heuristic "already in progress" check and the resume-with-nagging loop.
|
|
455
|
+
|
|
456
|
+
### Concurrency model: in-process worker pool + worktree-per-issue
|
|
457
|
+
|
|
458
|
+
**Decision** (recorded 2026-07-04, issue #1324): The durable engine uses an **in-process worker pool** model — a single control plane dispatches and monitors all concurrent issues, each isolated in its own git worktree.
|
|
459
|
+
|
|
460
|
+
**Rationale over process-per-issue:**
|
|
461
|
+
- Worktree isolation primitive already ships: `scripts/worktree-lifecycle.sh` (`ensure`/`cleanup` subcommands, merged #1268) provides deterministic filesystem isolation without forking a separate OS process per issue.
|
|
462
|
+
- A single control plane can enforce **shared rate-limit backpressure** across all in-flight issues; per-process models require IPC to share API quota state.
|
|
463
|
+
- Co-ordination primitives (DAG ready-set, completion callbacks, lease renewal) live in one place with no cross-process synchronisation overhead.
|
|
464
|
+
- Aligns with the engine-first inversion (#1256): the engine owns correctness; the spec owns routing.
|
|
465
|
+
|
|
466
|
+
**Filesystem isolation**: Before dispatching each issue, the engine calls:
|
|
467
|
+
```bash
|
|
468
|
+
scripts/worktree-lifecycle.sh ensure <issue-number> <lane>
|
|
469
|
+
# → creates or reuses .forgedock/worktrees/issue-<number>/
|
|
470
|
+
```
|
|
471
|
+
On completion or failure:
|
|
472
|
+
```bash
|
|
473
|
+
scripts/worktree-lifecycle.sh cleanup <issue-number>
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Concurrency cap** (`forge.yaml → orchestration.max_concurrent`):
|
|
477
|
+
- Default: uncapped — all DAG-ready issues dispatch simultaneously (preserves current behaviour).
|
|
478
|
+
- When `max_concurrent: N` is set, the dispatch loop holds at most N in-flight workers. Newly ready issues queue and start as running workers complete.
|
|
479
|
+
- Prevents wave-triggered rate-limit storms on large batches (e.g., 40-issue milestone dispatches).
|
|
480
|
+
|
|
481
|
+
**Rate-limit backpressure** (pre-dispatch gate):
|
|
482
|
+
|
|
483
|
+
Before dispatching each new worker, the engine runs:
|
|
484
|
+
```bash
|
|
485
|
+
REMAINING=$(gh api rate_limit --jq '.resources.core.remaining')
|
|
486
|
+
RESET_AT=$(gh api rate_limit --jq '.resources.core.reset')
|
|
487
|
+
RATE_LIMIT_FLOOR=${FORGE_RATE_LIMIT_FLOOR:-200}
|
|
488
|
+
|
|
489
|
+
if [ "$REMAINING" -lt "$RATE_LIMIT_FLOOR" ]; then
|
|
490
|
+
echo "GitHub API headroom below floor ($REMAINING < $RATE_LIMIT_FLOOR). Pausing dispatch until reset at $RESET_AT."
|
|
491
|
+
# Pause dispatch loop — already-running workers continue unaffected
|
|
492
|
+
sleep_until "$RESET_AT"
|
|
493
|
+
fi
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
- `FORGE_RATE_LIMIT_FLOOR` defaults to 200 remaining requests. Override in `forge.yaml → orchestration.rate_limit_floor`.
|
|
497
|
+
- Already-in-flight workers are **never interrupted** by the backpressure gate — only new dispatches pause.
|
|
498
|
+
- The gate is re-checked after each worker completion, not on a timer, so dispatch resumes immediately once the floor is cleared.
|
|
499
|
+
|
|
500
|
+
**Configuration reference** (`forge.yaml`):
|
|
501
|
+
```yaml
|
|
502
|
+
orchestration:
|
|
503
|
+
max_concurrent: 8 # optional; default: uncapped
|
|
504
|
+
rate_limit_floor: 200 # optional; default: 200
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Background Dispatch Mode <!-- Added: forge#1251 -->
|
|
510
|
+
|
|
511
|
+
This section governs how the orchestrator dispatches DAG-ready issues as background agents and how it handles wake/compaction recovery. Read it before every Phase 4 dispatch decision.
|
|
512
|
+
|
|
513
|
+
### Feature gate
|
|
514
|
+
|
|
515
|
+
Background dispatch (via `run_in_background=true` on each `Agent()` call) is the primary dispatch path. It is enabled when **both** of the following conditions hold:
|
|
516
|
+
|
|
517
|
+
1. **Version**: Claude Code >= v2.1.186 (the release that introduced background subagents with proper `agent_completed` completion notifications and the Notification hook). Below this version, background agents may not surface completion events correctly.
|
|
518
|
+
2. **Env var**: `CLAUDE_CODE_DISABLE_BACKGROUND_TASKS` is **not** set (or is empty).
|
|
519
|
+
|
|
520
|
+
Check both at the start of Phase 4, before the first dispatch:
|
|
521
|
+
|
|
522
|
+
```bash
|
|
523
|
+
# Feature gate check — run once before Phase 4 dispatch begins
|
|
524
|
+
BACKGROUND_DISPATCH_ENABLED=true
|
|
525
|
+
|
|
526
|
+
if [ -n "${CLAUDE_CODE_DISABLE_BACKGROUND_TASKS:-}" ]; then
|
|
527
|
+
echo "Background dispatch disabled: CLAUDE_CODE_DISABLE_BACKGROUND_TASKS is set."
|
|
528
|
+
BACKGROUND_DISPATCH_ENABLED=false
|
|
529
|
+
fi
|
|
530
|
+
|
|
531
|
+
# Version check: if the Claude Code version can be read, compare it.
|
|
532
|
+
# If the version cannot be determined, default to ENABLED (optimistic).
|
|
533
|
+
CC_VERSION=$(claude --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "")
|
|
534
|
+
if [ -n "$CC_VERSION" ]; then
|
|
535
|
+
# Compare major.minor.patch numerically
|
|
536
|
+
IFS='.' read -r CC_MAJOR CC_MINOR CC_PATCH <<< "$CC_VERSION"
|
|
537
|
+
if [ "$CC_MAJOR" -lt 2 ] || \
|
|
538
|
+
{ [ "$CC_MAJOR" -eq 2 ] && [ "$CC_MINOR" -lt 1 ]; } || \
|
|
539
|
+
{ [ "$CC_MAJOR" -eq 2 ] && [ "$CC_MINOR" -eq 1 ] && [ "$CC_PATCH" -lt 186 ]; }; then
|
|
540
|
+
echo "Background dispatch disabled: Claude Code ${CC_VERSION} < v2.1.186."
|
|
541
|
+
BACKGROUND_DISPATCH_ENABLED=false
|
|
542
|
+
fi
|
|
543
|
+
fi
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
**When `BACKGROUND_DISPATCH_ENABLED=false`**: fall back to the current streaming dispatch behavior — `run_in_background=true` is still set on each `Agent()` call (existing behavior, already correct), but treat completions as synchronous and do not rely on `agent_completed` notifications. Poll issue labels for terminal state instead.
|
|
547
|
+
|
|
548
|
+
**When `BACKGROUND_DISPATCH_ENABLED=true`**: use `run_in_background=true` (already in the Step 4A Agent() template) and react to `agent_completed` notifications as documented in Step 4B. Do NOT poll.
|
|
549
|
+
|
|
550
|
+
### Orchestrator state reconstruction on wake / after compaction
|
|
551
|
+
|
|
552
|
+
The orchestrator context window must stay small regardless of how many issues have been dispatched. Achieving this requires that all dispatch state is stored on GitHub — not in the orchestrator's context.
|
|
553
|
+
|
|
554
|
+
**Contract**: After any compaction event or orchestrator wake (session resumed after idle/restart), do NOT rely on in-context variables. Instead, reconstruct the DAG dispatch state from GitHub before checking for newly ready issues:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
# Reconstruct dispatch state from GitHub after compaction / wake
|
|
558
|
+
# Run this block at the top of every resumed Phase 4 loop iteration.
|
|
559
|
+
|
|
560
|
+
# 1. Re-fetch all issue labels to rebuild terminal-state knowledge
|
|
561
|
+
for NUM in {all_issue_numbers_in_batch}; do
|
|
562
|
+
ISSUE_STATE=$(gh issue view "$NUM" -R {GH_REPO} --json state,labels \
|
|
563
|
+
--jq '{state: .state, workflow: [.labels[].name | select(startswith("workflow:"))]}' \
|
|
564
|
+
2>/dev/null || echo '{}')
|
|
565
|
+
# Classify: terminal if workflow:merged, workflow:invalid, needs-human, or CLOSED
|
|
566
|
+
if echo "$ISSUE_STATE" | grep -qE 'workflow:merged|workflow:invalid|needs-human|CLOSED'; then
|
|
567
|
+
TERMINAL_ISSUES+=("$NUM")
|
|
568
|
+
else
|
|
569
|
+
ACTIVE_ISSUES+=("$NUM")
|
|
570
|
+
fi
|
|
571
|
+
done
|
|
572
|
+
|
|
573
|
+
# 2. Re-derive the ready set: any non-terminal issue whose predecessors are all terminal
|
|
574
|
+
for NUM in "${ACTIVE_ISSUES[@]}"; do
|
|
575
|
+
ALL_PREDS_DONE=true
|
|
576
|
+
for PRED in {predecessors_of_NUM}; do
|
|
577
|
+
if ! printf '%s\n' "${TERMINAL_ISSUES[@]}" | grep -qx "$PRED"; then
|
|
578
|
+
ALL_PREDS_DONE=false
|
|
579
|
+
break
|
|
580
|
+
fi
|
|
581
|
+
done
|
|
582
|
+
$ALL_PREDS_DONE && READY_ISSUES+=("$NUM")
|
|
583
|
+
done
|
|
584
|
+
|
|
585
|
+
# 3. Dispatch the reconstructed ready set via standard Step 4A.pre.0 → 4A.pre → 4A flow
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Why this keeps context small**: Each `Agent()` call returns an agent ID stored only in `AGENT_ISSUE_MAP`, which is rebuilt per Step 4A.pre dispatch batch. After compaction, the map is gone — but the DAG state is fully on GitHub. The reconstruction above re-derives the ready set from labels alone, so the orchestrator context never needs to hold cumulative dispatch history.
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|