forgedock 1.8.1 → 1.8.3
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/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +3 -2
- package/commands/orchestrate/phase-1-resolve.md +57 -12
- package/commands/orchestrate/phase-2-triage.md +1 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
- package/commands/orchestrate/phase-3-dependency.md +17 -5
- package/commands/orchestrate/phase-4-execution.md +625 -78
- package/commands/orchestrate/phase-5-cleanup.md +5 -2
- package/commands/orchestrate/phase-6-report.md +8 -3
- package/commands/orchestrate/safety.md +1 -0
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/config.md +1 -0
- package/commands/pipeline-health/phase-1-context.md +1 -0
- package/commands/pipeline-health/phase-2-metrics.md +1 -0
- package/commands/pipeline-health/phase-3-analyze.md +1 -0
- package/commands/pipeline-health/phase-4-proposals.md +7 -4
- package/commands/pipeline-health/phase-5-report.md +5 -2
- package/commands/pipeline-health/phase-6-summary.md +1 -0
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +219 -13
- package/commands/review-pr-agents/api.md +1 -0
- package/commands/review-pr-agents/auth.md +1 -0
- package/commands/review-pr-agents/billing.md +1 -0
- package/commands/review-pr-agents/concurrency.md +1 -0
- package/commands/review-pr-agents/database.md +1 -0
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +1 -0
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents/scraper.md +1 -0
- package/commands/review-pr-agents/security.md +1 -0
- package/commands/review-pr-agents/spec-cli.md +1 -0
- package/commands/review-pr-agents.md +10 -4
- package/commands/review-pr-staging.md +14 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +82 -19
- package/commands/work-on/build/context.md +62 -18
- package/commands/work-on/build/implement.md +63 -18
- package/commands/work-on/build/validate.md +241 -60
- package/commands/work-on/build.md +345 -146
- package/commands/work-on/close.md +618 -246
- package/commands/work-on/decompose.md +240 -41
- package/commands/work-on/investigate.md +378 -61
- package/commands/work-on/remediate.md +43 -9
- package/commands/work-on/review.md +241 -29
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +21 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
- package/commands/work-on-monolithic.md +0 -326
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
---
|
|
2
|
+
user-invocable: false
|
|
2
3
|
description: Decompose subcommand — break a complex issue into ordered sub-issues, post FORGE:DECOMPOSED, stop
|
|
3
|
-
|
|
4
|
+
context: fork
|
|
5
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
4
6
|
---
|
|
5
7
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
8
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
9
|
|
|
8
10
|
# work-on/decompose — Decomposition Subcommand
|
|
9
11
|
|
|
12
|
+
> **Skill Name Resolution (forked phase)**: `{FORGE_SKILL_PREFIX}` is the namespace this skill itself was invoked under — invoked as `forgedock:work-on:…` → `forgedock:` (nesting `:`); as `work-on:…` → empty (`install.sh`); as `forge-work-on-…` → `forge-` (Codex, nesting `-`); OpenCode → empty with `-` nesting. Confirm the target name in the available-skills list before calling it. A forked phase receives no resolved value from its caller; never guess, and if the target skill is not listed return BLOCKED "skill not found: <name>".
|
|
13
|
+
|
|
10
14
|
**Input**: $ARGUMENTS
|
|
11
15
|
|
|
12
|
-
**Invoked by**:
|
|
13
|
-
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set
|
|
16
|
+
**Invoked by**: the work-on router, when `INVESTIGATE_RESULT.decompose = YES`. This skill runs in an isolated forked context: it sees only its args and re-reads everything else from GitHub.
|
|
17
|
+
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set `workflow:decomposed`. STOP — each sub-issue runs its own /work-on. This skill is the SOLE owner of the `workflow:decomposed` label. On BLOCKED it adds `needs-human` and posts the blocker.
|
|
14
18
|
|
|
15
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are
|
|
19
|
+
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are registered as a real phase in the headless engine's phase table — `decompose` in `packages/protocol/src/phases.js`'s `PHASE_IDS`/`PHASE_MARKERS`, and the matching `decompose` entry in `bin/engine/phases.mjs`'s `PHASES` array. The engine's `investigate` phase hands off to it on `DECOMPOSE:YES` instead of terminating in place, so a headless `runIssue()` walk dispatches this subcommand rather than stopping short.
|
|
16
20
|
|
|
17
|
-
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation
|
|
21
|
+
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (label transitions, sub-issue creation) stay at this tier because they're interleaved with the reasoning-heavy sub-issue design steps in the same `Skill()` invocation. <!-- Added: forge#1827 -->
|
|
18
22
|
**NEVER use plan mode (EnterPlanMode).**
|
|
19
23
|
|
|
20
24
|
---
|
|
@@ -23,8 +27,111 @@ argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG]"
|
|
|
23
27
|
|
|
24
28
|
Parse from $ARGUMENTS:
|
|
25
29
|
- `{NUMBER}` — issue number (required)
|
|
26
|
-
- `--repo {GH_REPO}` — GitHub repo
|
|
27
|
-
- `--gh-flag {GH_FLAG}` — gh CLI repo flag
|
|
30
|
+
- `--repo {GH_REPO}` — GitHub repo, e.g. `{owner}/{repo}` (required)
|
|
31
|
+
- `--gh-flag {GH_FLAG}` — gh CLI repo flag, e.g. `-R {owner}/{repo}` (required)
|
|
32
|
+
|
|
33
|
+
If `{NUMBER}`, `--repo` or `--gh-flag` is missing, print the result block with `status: BLOCKED`, blocker: "missing required arg: <name>", and STOP (no GitHub write is possible without them).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Script resolution
|
|
38
|
+
|
|
39
|
+
**Shell state does not persist between Bash tool calls** (each call is a fresh shell): paste the block below at the top of every bash command in this skill that calls `resolve_script` or uses `FORGE_ROOT`/`UNIVERSAL_DIR`.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
43
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
44
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
45
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
46
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
47
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
48
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
49
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
50
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
51
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
52
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
53
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
54
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
55
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
56
|
+
ADAPTIVE_ENABLED=false
|
|
57
|
+
fi
|
|
58
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
59
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
60
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
61
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
62
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
63
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
64
|
+
FORGE_ROOT=""
|
|
65
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
66
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
67
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
68
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
69
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
|
|
70
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
71
|
+
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
72
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
73
|
+
while IFS= read -r _c; do
|
|
74
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
75
|
+
done <<< "$_k"
|
|
76
|
+
fi
|
|
77
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
78
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
79
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
80
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
81
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
82
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
83
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
84
|
+
# which is always safe and available. A missing script is never a reason
|
|
85
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
86
|
+
|
|
87
|
+
resolve_script() {
|
|
88
|
+
local operation="$1"
|
|
89
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
90
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
91
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
92
|
+
return
|
|
93
|
+
fi
|
|
94
|
+
# Tier 3: universal script
|
|
95
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
96
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
97
|
+
return
|
|
98
|
+
fi
|
|
99
|
+
# Tier 4: prose fallback
|
|
100
|
+
echo "prose:"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Result emission
|
|
107
|
+
|
|
108
|
+
Every exit path of this skill — COMPLETE, ALREADY_DONE, every guard, every failure — ends by printing exactly one block as the final reply:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
DECOMPOSE_RESULT:
|
|
112
|
+
status: COMPLETE | ALREADY_DONE | BLOCKED
|
|
113
|
+
sub_issues: [N, N, ...]
|
|
114
|
+
comment_url: {url of the FORGE:DECOMPOSED comment; empty if none}
|
|
115
|
+
blocker: {description if status=BLOCKED, else empty}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`sub_issues` is a plain list of issue numbers on every path (`[]` when none), never objects.
|
|
119
|
+
|
|
120
|
+
### Blocked exit
|
|
121
|
+
|
|
122
|
+
Every `BLOCKED` exit first runs this procedure (set `BLOCKER` to the reason text), then prints the result block with `status: BLOCKED`. It guarantees the parent never keeps a terminal `workflow:decomposed` label without sub-issues:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
BLOCKER="{reason}"
|
|
126
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
127
|
+
run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" --remove-label "workflow:decomposed" 2>/dev/null || true
|
|
128
|
+
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSE_BLOCKED -->
|
|
129
|
+
## Decomposition Blocked
|
|
130
|
+
|
|
131
|
+
${BLOCKER}
|
|
132
|
+
|
|
133
|
+
Human attention required (\`needs-human\`)."
|
|
134
|
+
```
|
|
28
135
|
|
|
29
136
|
---
|
|
30
137
|
|
|
@@ -40,9 +147,21 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
40
147
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
|
|
41
148
|
```
|
|
42
149
|
|
|
43
|
-
**
|
|
44
|
-
|
|
45
|
-
|
|
150
|
+
**MANDATORY — Owner override detection**: After reading the investigation comment, read ALL other comments on the issue to check for owner override signals:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
154
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Scan non-agent comments for override signals — phrases like "do not", "do NOT", "instead", "revert", "remove this", "override", "actually", or explicit disagreement with the investigation's recommendation. If an override comment is found from a repo owner or admin (not a bot):
|
|
158
|
+
|
|
159
|
+
1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
|
|
160
|
+
2. **Re-derive sub-issue scopes**: Derive sub-issue titles, bodies, and file scope from the override direction — NOT from the original investigation recommendation. The investigation's Decomposition Assessment may list sub-issues that are now stale or contradictory with the override.
|
|
161
|
+
3. **If override makes a sub-issue obsolete**: Skip creating it. Note the skip reason in the decomposition comment (Phase D5).
|
|
162
|
+
4. **If override changes the sequencing dependency**: Revise the execution order so that the override's primary action (e.g., "strip the feature") completes before any downstream doc/SDK sub-issues are built against it.
|
|
163
|
+
|
|
164
|
+
**Why this matters**: Sub-issues scoped before an override are built against a stale premise. A docs sub-issue scoped as "neutralize liability language" becomes incorrect if the upstream schema sub-issue will fully remove the feature — the docs sub-issue should instead be "remove all references to the deleted feature." Building both in parallel against the pre-override scope produces contradictory staging state.
|
|
46
165
|
|
|
47
166
|
> **Shared Scoping Convention**: This investigation gate (blocks without a `FORGE:INVESTIGATOR` comment) is the **reference pattern** for investigation-gated issue creation across the pipeline. `milestone.md` Step 4 and `orchestrate.md` MUST apply the same principle: read code and identify all affected call sites BEFORE writing any issue body. Sub-issue bodies created in Phase D3 MUST use the Pipeline Issue Template defined in `issue.md` Phase 3D — that template is the single canonical standard for all automated issue creation. <!-- Added: forge#293 -->
|
|
48
167
|
|
|
@@ -58,14 +177,40 @@ MILESTONE_TITLE=$(gh issue view {NUMBER} {GH_FLAG} --json milestone --jq '.miles
|
|
|
58
177
|
|
|
59
178
|
---
|
|
60
179
|
|
|
61
|
-
## Phase D1: Resume Check
|
|
180
|
+
## Phase D1: Guards and Resume Check
|
|
181
|
+
|
|
182
|
+
Evaluate in this order. Each BLOCKED exit runs the Blocked exit procedure.
|
|
183
|
+
|
|
184
|
+
**Guard 1 — issue is itself a sub-issue**: an issue whose body carries a `**Parent**: #N` reference is a child created by an earlier decomposition and is never decomposed again (prevents recursive decomposition). <!-- Added: forge#2379 -->
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
188
|
+
PARENT_REF=$(printf '%s\n' "$ISSUE_BODY" | grep -E '^\*\*Parent\*\*: #[0-9]+' | head -1)
|
|
189
|
+
[ -n "$PARENT_REF" ] && echo "SUB_ISSUE_GUARD: ${PARENT_REF}"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
If `PARENT_REF` is non-empty → BLOCKED, blocker: "Issue #{NUMBER} is a sub-issue (${PARENT_REF}) — sub-issues are never decomposed again; run /work-on build on it directly".
|
|
193
|
+
|
|
194
|
+
**Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent → BLOCKED, blocker: "No investigation report found — run investigate first".
|
|
195
|
+
|
|
196
|
+
**Guard 3 — decomposition plan present**: If the investigation report has no Decomposition Assessment section, OR the assessment does not list any sub-issues → BLOCKED, blocker: "Investigation report has no decomposition plan — re-run investigate with explicit decomposition scope".
|
|
197
|
+
|
|
198
|
+
**Resume check**:
|
|
62
199
|
|
|
63
200
|
```bash
|
|
64
201
|
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
65
|
-
--jq '.[] | select(.body | contains("FORGE:DECOMPOSED")) | .body'
|
|
202
|
+
--jq '[.[] | select(.body | contains("<!-- FORGE:DECOMPOSED -->"))] | last // empty | {url: .html_url, body: .body}'
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If a `<!-- FORGE:DECOMPOSED -->` comment exists → decomposition already complete. Derive `sub_issues` as a plain number list from that comment's `- #N:` lines and the comment URL from `.url`:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# $DECOMP_BODY is the .body of the comment found above
|
|
209
|
+
SUB_LIST=$(printf '%s\n' "$DECOMP_BODY" | sed -nE 's/^- #([0-9]+).*/\1/p' | paste -sd, - | sed 's/,/, /g')
|
|
210
|
+
echo "sub_issues: [${SUB_LIST}]"
|
|
66
211
|
```
|
|
67
212
|
|
|
68
|
-
|
|
213
|
+
Run Phase D6 (idempotent label re-assert), then EXIT with `DECOMPOSE_RESULT: status: ALREADY_DONE`, `sub_issues: [${SUB_LIST}]`, `comment_url` from the comment.
|
|
69
214
|
|
|
70
215
|
---
|
|
71
216
|
|
|
@@ -91,24 +236,26 @@ If `GIST_URLS` is non-empty, a `## Prior Investigation` section will be appended
|
|
|
91
236
|
|
|
92
237
|
## Phase D2: Design Sub-Issues
|
|
93
238
|
|
|
94
|
-
From the Decomposition Assessment in the investigation report, extract:
|
|
239
|
+
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), extract:
|
|
95
240
|
1. Sub-issue titles (in dependency order — independent issues first)
|
|
96
241
|
2. Dependencies between sub-issues (if issue B depends on issue A, A is created first)
|
|
97
242
|
3. Brief description for each sub-issue body
|
|
98
243
|
|
|
99
244
|
For each sub-issue, prepare:
|
|
100
|
-
- **Title**: from investigation report's proposed sub-issue title, including the final `{fix|feat|refactor}: ` prefix used in Phase D3
|
|
245
|
+
- **Title**: from investigation report's proposed sub-issue title (re-derived from the owner override when one was detected), including the final `{fix|feat|refactor}: ` prefix used in Phase D3
|
|
101
246
|
- **Body**: brief description of scope + `**Parent**: #{NUMBER}` + dependency note if applicable
|
|
102
247
|
- **Labels**: inherit priority label (P0/P1/P2) from parent; do NOT copy workflow labels
|
|
103
248
|
- **Milestone**: same milestone title as parent (if parent has one)
|
|
104
249
|
|
|
250
|
+
Sub-issues made obsolete by an owner override are not created; record each as `SKIPPED_NOTES` (title + reason) for the Phase D5 comment.
|
|
251
|
+
|
|
105
252
|
**Ordering rule**: Create independent sub-issues first. If sub-issue B depends on A, create A first so its issue number can be referenced in B's body.
|
|
106
253
|
|
|
107
254
|
---
|
|
108
255
|
|
|
109
256
|
## Phase D2.5: Equivalent Set Check
|
|
110
257
|
|
|
111
|
-
Before creating any sub-issue, check whether the complete planned title set already exists for this parent. This covers an interrupted earlier decomposition: its child issues may exist even though it never reached the `FORGE:DECOMPOSED` comment checked in D1.
|
|
258
|
+
Before creating any sub-issue, check whether the complete planned title set already exists for this parent. This covers an interrupted earlier decomposition: its child issues may exist even though it never reached the `FORGE:DECOMPOSED` comment checked in Phase D1.
|
|
112
259
|
|
|
113
260
|
```bash
|
|
114
261
|
# SUB_TITLES contains the final, ordered titles prepared in D2.
|
|
@@ -124,14 +271,19 @@ CANONICAL_SUB_ISSUES=$(jq --arg parent "**Parent**: #{NUMBER}" --argjson expecte
|
|
|
124
271
|
| ($canonical | map(.title) | sort) as $actual
|
|
125
272
|
| if $actual == $expected then $canonical else empty end
|
|
126
273
|
' <<< "$PARENT_SUB_ISSUES")
|
|
274
|
+
|
|
275
|
+
# Plain number list for the result block, e.g. "101, 102"
|
|
276
|
+
CANONICAL_NUMBERS=$(printf '%s' "$CANONICAL_SUB_ISSUES" | jq -r 'map(.number) | sort | map(tostring) | join(", ")' 2>/dev/null)
|
|
127
277
|
```
|
|
128
278
|
|
|
129
|
-
If `CANONICAL_SUB_ISSUES` is non-empty, the decomposition has already created an equivalent set. Do **not** create issues, edit the parent body, or post another decomposition comment. Preserve those original sub-issues and EXIT with:
|
|
279
|
+
If `CANONICAL_SUB_ISSUES` is non-empty, the decomposition has already created an equivalent set. Do **not** create issues, edit the parent body, or post another decomposition comment. Preserve those original sub-issues, run Phase D6 (idempotent label re-assert), and EXIT with:
|
|
130
280
|
|
|
131
281
|
```
|
|
132
282
|
DECOMPOSE_RESULT:
|
|
133
283
|
status: ALREADY_DONE
|
|
134
|
-
sub_issues: {
|
|
284
|
+
sub_issues: [${CANONICAL_NUMBERS}]
|
|
285
|
+
comment_url:
|
|
286
|
+
blocker:
|
|
135
287
|
```
|
|
136
288
|
|
|
137
289
|
If no exact title-set match exists, continue. A partial set is not equivalent: let `/issue` dedup protect its existing children while creating only the missing work.
|
|
@@ -140,12 +292,19 @@ If no exact title-set match exists, continue. A partial set is not equivalent: l
|
|
|
140
292
|
|
|
141
293
|
## Phase D3: Create Sub-Issues
|
|
142
294
|
|
|
143
|
-
For each sub-issue (in dependency order), route creation through the `/issue` create-hook's programmatic invocation contract (`commands/issue.md` Programmatic Invocation Contract, added in #2085) instead of calling `gh issue create` directly. `/issue`'s Phase 2D runs the same `scripts/issue-dedup.sh` check this file used to run manually —
|
|
295
|
+
For each sub-issue (in dependency order), route creation through the `/issue` create-hook's programmatic invocation contract (`commands/issue.md` Programmatic Invocation Contract, added in #2085) instead of calling `gh issue create` directly. `/issue`'s Phase 2D runs the same `scripts/issue-dedup.sh` check this file used to run manually — dedup is enforced inside the create-hook on every call, with no bypass path. <!-- Changed: forge#2086 — route through /issue create-hook -->
|
|
296
|
+
|
|
297
|
+
Initialise once before the loop: `CREATED_NUMBERS=""` (comma-separated, for the result block) and `SKIPPED_NOTES=""`.
|
|
144
298
|
|
|
145
|
-
**
|
|
299
|
+
**Sanitise the title and compose the sub-issue body to a temp file** (avoids quoting issues when passed as `--body-file`):
|
|
146
300
|
|
|
147
301
|
```bash
|
|
148
302
|
SUB_TITLE="{fix|feat|refactor}: {SUB_ISSUE_TITLE}"
|
|
303
|
+
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
304
|
+
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
305
|
+
# this is no longer required for safety — but strip it anyway so the raw title
|
|
306
|
+
# stays readable if it round-trips through any other eval-based consumer.
|
|
307
|
+
SUB_TITLE=$(printf '%s' "$SUB_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
149
308
|
SUB_BODY_FILE="$(mktemp)"
|
|
150
309
|
cat > "$SUB_BODY_FILE" <<'SUB_BODY_EOF'
|
|
151
310
|
## Problem
|
|
@@ -175,23 +334,52 @@ Files that need changes:
|
|
|
175
334
|
SUB_BODY_EOF
|
|
176
335
|
```
|
|
177
336
|
|
|
337
|
+
**Build the milestone arg** (only when the parent has a milestone — a parent with no milestone must not receive `--milestone ""`):
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
MILESTONE_ARG=""
|
|
341
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_ARG=" --milestone \"${MILESTONE_TITLE}\""
|
|
342
|
+
```
|
|
343
|
+
|
|
178
344
|
**Invoke `/issue` in programmatic mode**:
|
|
179
345
|
|
|
180
346
|
```
|
|
181
|
-
ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"
|
|
347
|
+
ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"${MILESTONE_ARG}"))
|
|
182
348
|
```
|
|
183
349
|
|
|
350
|
+
If the skill is not found → BLOCKED, blocker: "skill not found: issue".
|
|
351
|
+
|
|
184
352
|
`/issue` runs Phase 2D dedup, Phase 3F body validation, then creates the issue (Phase 4) — no separate pre-check needed on this side.
|
|
185
353
|
|
|
186
|
-
|
|
354
|
+
If the `--milestone` flag fails (milestone not found by name): re-invoke without `${MILESTONE_ARG}`, set `MILESTONE_SKIPPED=1`, and note in the FORGE:DECOMPOSED comment that milestone assignment was skipped.
|
|
355
|
+
|
|
356
|
+
**Extract the created sub-issue number from the Skill output** (see `commands/issue.md` Phase 4C/4E — it echoes `Created: {url}` and reports `**#{NUMBER}**: {title}`). If parsing yields nothing, fall back to an exact-title search scoped to this parent (covers `/issue` output not being captured by the Skill harness):
|
|
187
357
|
|
|
188
358
|
```bash
|
|
189
359
|
# Match either the "Created: {url}" line (extract trailing /issues/N) or the "**#{NUMBER}**" bold report line.
|
|
190
360
|
SUB_NUMBER=$(echo "$ISSUE_SKILL_OUTPUT" | grep -oE 'issues/[0-9]+' | head -1 | grep -oE '[0-9]+')
|
|
191
361
|
[ -z "$SUB_NUMBER" ] && SUB_NUMBER=$(echo "$ISSUE_SKILL_OUTPUT" | grep -oE '\*\*#[0-9]+\*\*' | head -1 | grep -oE '[0-9]+')
|
|
192
362
|
|
|
363
|
+
# Fallback: exact-title search, restricted to issues whose body carries this parent's reference
|
|
364
|
+
# (so a pre-existing near-duplicate that /issue dedup stopped on is never mistaken for the new child).
|
|
365
|
+
# Retries absorb GitHub Search API indexing lag.
|
|
366
|
+
if [ -z "$SUB_NUMBER" ]; then
|
|
367
|
+
for _resolve_attempt in 1 2 3; do
|
|
368
|
+
SUB_NUMBER=$(gh issue list {GH_FLAG} --search "in:title \"${SUB_TITLE}\"" --state open --limit 10 --json number,title,body 2>/dev/null \
|
|
369
|
+
| jq -r --arg t "$SUB_TITLE" --arg p "**Parent**: #{NUMBER}" '[.[] | select(.title == $t and (.body | contains($p)))][0].number // empty')
|
|
370
|
+
[ -n "$SUB_NUMBER" ] && break
|
|
371
|
+
sleep 2
|
|
372
|
+
done
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
rm -f "$SUB_BODY_FILE"
|
|
376
|
+
|
|
193
377
|
if [ -z "$SUB_NUMBER" ]; then
|
|
194
378
|
echo "WARNING: /issue did not report a created issue number for sub-issue '${SUB_TITLE}' — likely a Phase 2D dedup STOP (near-duplicate found) or a usage error. Skipping this sub-issue: do not reference it in the parent tracker or in dependent sub-issue bodies. Review the Skill output above and, if a near-duplicate exists, comment on the existing issue instead."
|
|
379
|
+
SKIPPED_NOTES="${SKIPPED_NOTES}
|
|
380
|
+
- ${SUB_TITLE} — not created (dedup stop or /issue error)"
|
|
381
|
+
else
|
|
382
|
+
CREATED_NUMBERS="${CREATED_NUMBERS:+${CREATED_NUMBERS}, }${SUB_NUMBER}"
|
|
195
383
|
fi
|
|
196
384
|
```
|
|
197
385
|
|
|
@@ -221,9 +409,7 @@ Investigation findings from the parent issue are available as Knowledge Gists:
|
|
|
221
409
|
fi
|
|
222
410
|
```
|
|
223
411
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
If `--milestone` flag fails (milestone not found by name): omit the flag and note in the FORGE:DECOMPOSED comment that milestone assignment was skipped.
|
|
412
|
+
After the loop, if `CREATED_NUMBERS` is empty (no sub-issue was created) → BLOCKED, blocker: "No sub-issues were created (all skipped or deduplicated) — review the /issue output".
|
|
227
413
|
|
|
228
414
|
---
|
|
229
415
|
|
|
@@ -245,7 +431,7 @@ TRACKER="
|
|
|
245
431
|
- [ ] #{SUB_ISSUE_2_NUMBER} — {SUB_ISSUE_2_TITLE} _(depends on #{SUB_ISSUE_1_NUMBER})_
|
|
246
432
|
..."
|
|
247
433
|
|
|
248
|
-
gh issue edit {NUMBER} {GH_FLAG} --body "${CURRENT_BODY}${TRACKER}"
|
|
434
|
+
gh issue edit {NUMBER} {GH_FLAG} --body "${CURRENT_BODY}${TRACKER}" # allowlist:check-command-side-effects
|
|
249
435
|
```
|
|
250
436
|
|
|
251
437
|
---
|
|
@@ -259,8 +445,11 @@ SHOW_ATTRIBUTION=$(yq '.branding.show_attribution // "true"' forge.yaml 2>/dev/n
|
|
|
259
445
|
> Pipeline powered by [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock)"
|
|
260
446
|
```
|
|
261
447
|
|
|
448
|
+
Compose the comment body. Include the `### Skipped Sub-Issues` section only when `SKIPPED_NOTES` is non-empty (each owner-override-obsolete sub-issue and each not-created sub-issue, with its reason), an `### Owner Override` line when Phase D0 detected one (the direction the owner steered), and a milestone-skipped note when `MILESTONE_SKIPPED=1`.
|
|
449
|
+
|
|
262
450
|
```bash
|
|
263
|
-
|
|
451
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
452
|
+
COMMENT_URL=$(gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
|
|
264
453
|
## Decomposition Complete
|
|
265
454
|
|
|
266
455
|
This issue has been broken into sub-issues. Each sub-issue runs through its own /work-on pipeline independently.
|
|
@@ -270,21 +459,39 @@ This issue has been broken into sub-issues. Each sub-issue runs through its own
|
|
|
270
459
|
{for each sub-issue, in dependency order:}
|
|
271
460
|
- #{SUB_ISSUE_NUMBER}: {TITLE}{if has dependency: _(depends on #{DEP_NUMBER})_}
|
|
272
461
|
|
|
462
|
+
{if SKIPPED_NOTES non-empty:}
|
|
463
|
+
### Skipped Sub-Issues
|
|
464
|
+
|
|
465
|
+
${SKIPPED_NOTES}
|
|
466
|
+
|
|
467
|
+
{if owner override detected:}
|
|
468
|
+
### Owner Override
|
|
469
|
+
|
|
470
|
+
{override direction and how it changed the sub-issue scopes/order}
|
|
471
|
+
|
|
273
472
|
### Decomposition Rationale
|
|
274
473
|
|
|
275
474
|
{brief summary of why this issue was decomposed and the dependency ordering chosen}
|
|
276
475
|
${ATTRIBUTION_LINE}
|
|
277
|
-
<!-- FORGE:DECOMPOSED:COMPLETE -->"
|
|
476
|
+
<!-- FORGE:DECOMPOSED:COMPLETE -->") # allowlist:check-command-side-effects
|
|
278
477
|
```
|
|
279
478
|
|
|
280
479
|
---
|
|
281
480
|
|
|
282
481
|
## Phase D6: Update Labels
|
|
283
482
|
|
|
483
|
+
Decompose is the SOLE owner of the `workflow:decomposed` label. Transition with tiered dispatch; a transition to `decomposed` removes every other workflow label. Also run this block (idempotently) on both ALREADY_DONE exits.
|
|
484
|
+
|
|
284
485
|
```bash
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
486
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
487
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
488
|
+
case "$TIER" in
|
|
489
|
+
adaptive|universal) run bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
|
|
490
|
+
prose)
|
|
491
|
+
run gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
|
|
492
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
493
|
+
;;
|
|
494
|
+
esac
|
|
288
495
|
```
|
|
289
496
|
|
|
290
497
|
---
|
|
@@ -293,15 +500,7 @@ gh issue edit {NUMBER} {GH_FLAG} \
|
|
|
293
500
|
|
|
294
501
|
Decomposition is a terminal route for the parent issue. Each sub-issue will be picked up separately by /work-on.
|
|
295
502
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
```
|
|
299
|
-
DECOMPOSE_RESULT:
|
|
300
|
-
status: COMPLETE | ALREADY_DONE | BLOCKED
|
|
301
|
-
sub_issues: [{number}, ...]
|
|
302
|
-
comment_url: {url of FORGE:DECOMPOSED comment}
|
|
303
|
-
blocker: {description if status=BLOCKED}
|
|
304
|
-
```
|
|
503
|
+
Print the result block (see Result emission) as the final reply, with `status: COMPLETE`, `sub_issues: [${CREATED_NUMBERS}]` (numbers only) and `comment_url: ${COMMENT_URL}`.
|
|
305
504
|
|
|
306
505
|
**Router behavior after DECOMPOSE_RESULT**: `break` — do not continue to build/review/close for the parent issue.
|
|
307
506
|
|
|
@@ -314,7 +513,7 @@ The subcommand writes its results to GitHub (FORGE:DECOMPOSED comment + sub-issu
|
|
|
314
513
|
```
|
|
315
514
|
DECOMPOSE_RESULT:
|
|
316
515
|
status: COMPLETE | ALREADY_DONE | BLOCKED
|
|
317
|
-
sub_issues: [
|
|
516
|
+
sub_issues: [N, N, ...]
|
|
318
517
|
comment_url: {url of posted FORGE:DECOMPOSED comment}
|
|
319
518
|
blocker: {description if status=BLOCKED}
|
|
320
519
|
```
|