forgedock 1.8.0 → 1.8.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/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/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 +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- 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 +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -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 +4 -2
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- 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 +11 -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
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Decompose subcommand — break a complex issue into ordered sub-issues, post FORGE:DECOMPOSED, stop
|
|
3
|
-
|
|
3
|
+
context: fork
|
|
4
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
7
8
|
|
|
8
9
|
# work-on/decompose — Decomposition Subcommand
|
|
9
10
|
|
|
11
|
+
> **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>".
|
|
12
|
+
|
|
10
13
|
**Input**: $ARGUMENTS
|
|
11
14
|
|
|
12
|
-
**Invoked by**:
|
|
13
|
-
**Output**: Create sub-issues, update parent tracker, post `<!-- FORGE:DECOMPOSED -->` comment, set
|
|
15
|
+
**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.
|
|
16
|
+
**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
17
|
|
|
15
|
-
**Engine coverage** (forge#2379): this subcommand's `command` name (`work-on/decompose`) and completion marker (`FORGE:DECOMPOSED:COMPLETE`) are
|
|
18
|
+
**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
19
|
|
|
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
|
|
20
|
+
**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
21
|
**NEVER use plan mode (EnterPlanMode).**
|
|
19
22
|
|
|
20
23
|
---
|
|
@@ -23,8 +26,111 @@ argument-hint: "[issue number] [--repo GH_REPO] [--gh-flag GH_FLAG]"
|
|
|
23
26
|
|
|
24
27
|
Parse from $ARGUMENTS:
|
|
25
28
|
- `{NUMBER}` — issue number (required)
|
|
26
|
-
- `--repo {GH_REPO}` — GitHub repo
|
|
27
|
-
- `--gh-flag {GH_FLAG}` — gh CLI repo flag
|
|
29
|
+
- `--repo {GH_REPO}` — GitHub repo, e.g. `{owner}/{repo}` (required)
|
|
30
|
+
- `--gh-flag {GH_FLAG}` — gh CLI repo flag, e.g. `-R {owner}/{repo}` (required)
|
|
31
|
+
|
|
32
|
+
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).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Script resolution
|
|
37
|
+
|
|
38
|
+
**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`.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
42
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
43
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
44
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
45
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
46
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
47
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
48
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
49
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
50
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
51
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
52
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
53
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
54
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
55
|
+
ADAPTIVE_ENABLED=false
|
|
56
|
+
fi
|
|
57
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
58
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
59
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
60
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
61
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
62
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
63
|
+
FORGE_ROOT=""
|
|
64
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
65
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
66
|
+
_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")")"
|
|
67
|
+
# 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)
|
|
68
|
+
_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-)"
|
|
69
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
70
|
+
# '${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.
|
|
71
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
72
|
+
while IFS= read -r _c; do
|
|
73
|
+
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
|
|
74
|
+
done <<< "$_k"
|
|
75
|
+
fi
|
|
76
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
77
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
78
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
79
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
80
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
81
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
82
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
83
|
+
# which is always safe and available. A missing script is never a reason
|
|
84
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
85
|
+
|
|
86
|
+
resolve_script() {
|
|
87
|
+
local operation="$1"
|
|
88
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
89
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
90
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
91
|
+
return
|
|
92
|
+
fi
|
|
93
|
+
# Tier 3: universal script
|
|
94
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
95
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
# Tier 4: prose fallback
|
|
99
|
+
echo "prose:"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Result emission
|
|
106
|
+
|
|
107
|
+
Every exit path of this skill — COMPLETE, ALREADY_DONE, every guard, every failure — ends by printing exactly one block as the final reply:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
DECOMPOSE_RESULT:
|
|
111
|
+
status: COMPLETE | ALREADY_DONE | BLOCKED
|
|
112
|
+
sub_issues: [N, N, ...]
|
|
113
|
+
comment_url: {url of the FORGE:DECOMPOSED comment; empty if none}
|
|
114
|
+
blocker: {description if status=BLOCKED, else empty}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`sub_issues` is a plain list of issue numbers on every path (`[]` when none), never objects.
|
|
118
|
+
|
|
119
|
+
### Blocked exit
|
|
120
|
+
|
|
121
|
+
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:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
BLOCKER="{reason}"
|
|
125
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
126
|
+
run gh issue edit {NUMBER} {GH_FLAG} --add-label "needs-human" --remove-label "workflow:decomposed" 2>/dev/null || true
|
|
127
|
+
run gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSE_BLOCKED -->
|
|
128
|
+
## Decomposition Blocked
|
|
129
|
+
|
|
130
|
+
${BLOCKER}
|
|
131
|
+
|
|
132
|
+
Human attention required (\`needs-human\`)."
|
|
133
|
+
```
|
|
28
134
|
|
|
29
135
|
---
|
|
30
136
|
|
|
@@ -40,9 +146,21 @@ gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
|
40
146
|
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body'
|
|
41
147
|
```
|
|
42
148
|
|
|
43
|
-
**
|
|
44
|
-
|
|
45
|
-
|
|
149
|
+
**MANDATORY — Owner override detection**: After reading the investigation comment, read ALL other comments on the issue to check for owner override signals:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
153
|
+
--jq '.[] | select(.body | contains("FORGE:INVESTIGATOR") | not) | {author: .user.login, body: .body}'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
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):
|
|
157
|
+
|
|
158
|
+
1. **Document the override**: Note which direction the owner is steering (e.g., "remove the feature" vs. investigation's "keep with warnings")
|
|
159
|
+
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.
|
|
160
|
+
3. **If override makes a sub-issue obsolete**: Skip creating it. Note the skip reason in the decomposition comment (Phase D5).
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
**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
164
|
|
|
47
165
|
> **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
166
|
|
|
@@ -58,14 +176,40 @@ MILESTONE_TITLE=$(gh issue view {NUMBER} {GH_FLAG} --json milestone --jq '.miles
|
|
|
58
176
|
|
|
59
177
|
---
|
|
60
178
|
|
|
61
|
-
## Phase D1: Resume Check
|
|
179
|
+
## Phase D1: Guards and Resume Check
|
|
180
|
+
|
|
181
|
+
Evaluate in this order. Each BLOCKED exit runs the Blocked exit procedure.
|
|
182
|
+
|
|
183
|
+
**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 -->
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
187
|
+
PARENT_REF=$(printf '%s\n' "$ISSUE_BODY" | grep -E '^\*\*Parent\*\*: #[0-9]+' | head -1)
|
|
188
|
+
[ -n "$PARENT_REF" ] && echo "SUB_ISSUE_GUARD: ${PARENT_REF}"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
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".
|
|
192
|
+
|
|
193
|
+
**Guard 2 — investigation report present**: If the FORGE:INVESTIGATOR comment is absent → BLOCKED, blocker: "No investigation report found — run investigate first".
|
|
194
|
+
|
|
195
|
+
**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".
|
|
196
|
+
|
|
197
|
+
**Resume check**:
|
|
62
198
|
|
|
63
199
|
```bash
|
|
64
200
|
gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
65
|
-
--jq '.[] | select(.body | contains("FORGE:DECOMPOSED")) | .body'
|
|
201
|
+
--jq '[.[] | select(.body | contains("<!-- FORGE:DECOMPOSED -->"))] | last // empty | {url: .html_url, body: .body}'
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
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`:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# $DECOMP_BODY is the .body of the comment found above
|
|
208
|
+
SUB_LIST=$(printf '%s\n' "$DECOMP_BODY" | sed -nE 's/^- #([0-9]+).*/\1/p' | paste -sd, - | sed 's/,/, /g')
|
|
209
|
+
echo "sub_issues: [${SUB_LIST}]"
|
|
66
210
|
```
|
|
67
211
|
|
|
68
|
-
|
|
212
|
+
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
213
|
|
|
70
214
|
---
|
|
71
215
|
|
|
@@ -91,24 +235,26 @@ If `GIST_URLS` is non-empty, a `## Prior Investigation` section will be appended
|
|
|
91
235
|
|
|
92
236
|
## Phase D2: Design Sub-Issues
|
|
93
237
|
|
|
94
|
-
From the Decomposition Assessment in the investigation report, extract:
|
|
238
|
+
From the Decomposition Assessment in the investigation report (adjusted for any owner override detected in Phase D0), extract:
|
|
95
239
|
1. Sub-issue titles (in dependency order — independent issues first)
|
|
96
240
|
2. Dependencies between sub-issues (if issue B depends on issue A, A is created first)
|
|
97
241
|
3. Brief description for each sub-issue body
|
|
98
242
|
|
|
99
243
|
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
|
|
244
|
+
- **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
245
|
- **Body**: brief description of scope + `**Parent**: #{NUMBER}` + dependency note if applicable
|
|
102
246
|
- **Labels**: inherit priority label (P0/P1/P2) from parent; do NOT copy workflow labels
|
|
103
247
|
- **Milestone**: same milestone title as parent (if parent has one)
|
|
104
248
|
|
|
249
|
+
Sub-issues made obsolete by an owner override are not created; record each as `SKIPPED_NOTES` (title + reason) for the Phase D5 comment.
|
|
250
|
+
|
|
105
251
|
**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
252
|
|
|
107
253
|
---
|
|
108
254
|
|
|
109
255
|
## Phase D2.5: Equivalent Set Check
|
|
110
256
|
|
|
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.
|
|
257
|
+
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
258
|
|
|
113
259
|
```bash
|
|
114
260
|
# SUB_TITLES contains the final, ordered titles prepared in D2.
|
|
@@ -124,14 +270,19 @@ CANONICAL_SUB_ISSUES=$(jq --arg parent "**Parent**: #{NUMBER}" --argjson expecte
|
|
|
124
270
|
| ($canonical | map(.title) | sort) as $actual
|
|
125
271
|
| if $actual == $expected then $canonical else empty end
|
|
126
272
|
' <<< "$PARENT_SUB_ISSUES")
|
|
273
|
+
|
|
274
|
+
# Plain number list for the result block, e.g. "101, 102"
|
|
275
|
+
CANONICAL_NUMBERS=$(printf '%s' "$CANONICAL_SUB_ISSUES" | jq -r 'map(.number) | sort | map(tostring) | join(", ")' 2>/dev/null)
|
|
127
276
|
```
|
|
128
277
|
|
|
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:
|
|
278
|
+
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
279
|
|
|
131
280
|
```
|
|
132
281
|
DECOMPOSE_RESULT:
|
|
133
282
|
status: ALREADY_DONE
|
|
134
|
-
sub_issues: {
|
|
283
|
+
sub_issues: [${CANONICAL_NUMBERS}]
|
|
284
|
+
comment_url:
|
|
285
|
+
blocker:
|
|
135
286
|
```
|
|
136
287
|
|
|
137
288
|
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 +291,19 @@ If no exact title-set match exists, continue. A partial set is not equivalent: l
|
|
|
140
291
|
|
|
141
292
|
## Phase D3: Create Sub-Issues
|
|
142
293
|
|
|
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 —
|
|
294
|
+
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 -->
|
|
295
|
+
|
|
296
|
+
Initialise once before the loop: `CREATED_NUMBERS=""` (comma-separated, for the result block) and `SKIPPED_NOTES=""`.
|
|
144
297
|
|
|
145
|
-
**
|
|
298
|
+
**Sanitise the title and compose the sub-issue body to a temp file** (avoids quoting issues when passed as `--body-file`):
|
|
146
299
|
|
|
147
300
|
```bash
|
|
148
301
|
SUB_TITLE="{fix|feat|refactor}: {SUB_ISSUE_TITLE}"
|
|
302
|
+
# Defense-in-depth: /issue's arg tokenizer (commands/issue.md, forge#2094) uses
|
|
303
|
+
# an xargs-based tokenizer that never expands backtick/$(...) substitution, so
|
|
304
|
+
# this is no longer required for safety — but strip it anyway so the raw title
|
|
305
|
+
# stays readable if it round-trips through any other eval-based consumer.
|
|
306
|
+
SUB_TITLE=$(printf '%s' "$SUB_TITLE" | tr '`' "'" | sed 's/\$(/$ (/g')
|
|
149
307
|
SUB_BODY_FILE="$(mktemp)"
|
|
150
308
|
cat > "$SUB_BODY_FILE" <<'SUB_BODY_EOF'
|
|
151
309
|
## Problem
|
|
@@ -175,23 +333,52 @@ Files that need changes:
|
|
|
175
333
|
SUB_BODY_EOF
|
|
176
334
|
```
|
|
177
335
|
|
|
336
|
+
**Build the milestone arg** (only when the parent has a milestone — a parent with no milestone must not receive `--milestone ""`):
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
MILESTONE_ARG=""
|
|
340
|
+
[ -n "$MILESTONE_TITLE" ] && MILESTONE_ARG=" --milestone \"${MILESTONE_TITLE}\""
|
|
341
|
+
```
|
|
342
|
+
|
|
178
343
|
**Invoke `/issue` in programmatic mode**:
|
|
179
344
|
|
|
180
345
|
```
|
|
181
|
-
ISSUE_SKILL_OUTPUT=$(Skill(skill="issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"
|
|
346
|
+
ISSUE_SKILL_OUTPUT=$(Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"${SUB_TITLE}\" --body-file \"${SUB_BODY_FILE}\" --label \"{PRIORITY_LABEL}\"${MILESTONE_ARG}"))
|
|
182
347
|
```
|
|
183
348
|
|
|
349
|
+
If the skill is not found → BLOCKED, blocker: "skill not found: issue".
|
|
350
|
+
|
|
184
351
|
`/issue` runs Phase 2D dedup, Phase 3F body validation, then creates the issue (Phase 4) — no separate pre-check needed on this side.
|
|
185
352
|
|
|
186
|
-
|
|
353
|
+
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.
|
|
354
|
+
|
|
355
|
+
**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
356
|
|
|
188
357
|
```bash
|
|
189
358
|
# Match either the "Created: {url}" line (extract trailing /issues/N) or the "**#{NUMBER}**" bold report line.
|
|
190
359
|
SUB_NUMBER=$(echo "$ISSUE_SKILL_OUTPUT" | grep -oE 'issues/[0-9]+' | head -1 | grep -oE '[0-9]+')
|
|
191
360
|
[ -z "$SUB_NUMBER" ] && SUB_NUMBER=$(echo "$ISSUE_SKILL_OUTPUT" | grep -oE '\*\*#[0-9]+\*\*' | head -1 | grep -oE '[0-9]+')
|
|
192
361
|
|
|
362
|
+
# Fallback: exact-title search, restricted to issues whose body carries this parent's reference
|
|
363
|
+
# (so a pre-existing near-duplicate that /issue dedup stopped on is never mistaken for the new child).
|
|
364
|
+
# Retries absorb GitHub Search API indexing lag.
|
|
365
|
+
if [ -z "$SUB_NUMBER" ]; then
|
|
366
|
+
for _resolve_attempt in 1 2 3; do
|
|
367
|
+
SUB_NUMBER=$(gh issue list {GH_FLAG} --search "in:title \"${SUB_TITLE}\"" --state open --limit 10 --json number,title,body 2>/dev/null \
|
|
368
|
+
| jq -r --arg t "$SUB_TITLE" --arg p "**Parent**: #{NUMBER}" '[.[] | select(.title == $t and (.body | contains($p)))][0].number // empty')
|
|
369
|
+
[ -n "$SUB_NUMBER" ] && break
|
|
370
|
+
sleep 2
|
|
371
|
+
done
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
rm -f "$SUB_BODY_FILE"
|
|
375
|
+
|
|
193
376
|
if [ -z "$SUB_NUMBER" ]; then
|
|
194
377
|
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."
|
|
378
|
+
SKIPPED_NOTES="${SKIPPED_NOTES}
|
|
379
|
+
- ${SUB_TITLE} — not created (dedup stop or /issue error)"
|
|
380
|
+
else
|
|
381
|
+
CREATED_NUMBERS="${CREATED_NUMBERS:+${CREATED_NUMBERS}, }${SUB_NUMBER}"
|
|
195
382
|
fi
|
|
196
383
|
```
|
|
197
384
|
|
|
@@ -221,9 +408,7 @@ Investigation findings from the parent issue are available as Knowledge Gists:
|
|
|
221
408
|
fi
|
|
222
409
|
```
|
|
223
410
|
|
|
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.
|
|
411
|
+
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
412
|
|
|
228
413
|
---
|
|
229
414
|
|
|
@@ -245,7 +430,7 @@ TRACKER="
|
|
|
245
430
|
- [ ] #{SUB_ISSUE_2_NUMBER} — {SUB_ISSUE_2_TITLE} _(depends on #{SUB_ISSUE_1_NUMBER})_
|
|
246
431
|
..."
|
|
247
432
|
|
|
248
|
-
gh issue edit {NUMBER} {GH_FLAG} --body "${CURRENT_BODY}${TRACKER}"
|
|
433
|
+
gh issue edit {NUMBER} {GH_FLAG} --body "${CURRENT_BODY}${TRACKER}" # allowlist:check-command-side-effects
|
|
249
434
|
```
|
|
250
435
|
|
|
251
436
|
---
|
|
@@ -259,8 +444,11 @@ SHOW_ATTRIBUTION=$(yq '.branding.show_attribution // "true"' forge.yaml 2>/dev/n
|
|
|
259
444
|
> Pipeline powered by [ForgeDock](https://github.com/RapierCraftStudios/ForgeDock)"
|
|
260
445
|
```
|
|
261
446
|
|
|
447
|
+
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`.
|
|
448
|
+
|
|
262
449
|
```bash
|
|
263
|
-
|
|
450
|
+
[ "${DRY_RUN:-false}" = "true" ] && { echo "[DRY_RUN] would post the comment below"; exit 0; }
|
|
451
|
+
COMMENT_URL=$(gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DECOMPOSED -->
|
|
264
452
|
## Decomposition Complete
|
|
265
453
|
|
|
266
454
|
This issue has been broken into sub-issues. Each sub-issue runs through its own /work-on pipeline independently.
|
|
@@ -270,21 +458,39 @@ This issue has been broken into sub-issues. Each sub-issue runs through its own
|
|
|
270
458
|
{for each sub-issue, in dependency order:}
|
|
271
459
|
- #{SUB_ISSUE_NUMBER}: {TITLE}{if has dependency: _(depends on #{DEP_NUMBER})_}
|
|
272
460
|
|
|
461
|
+
{if SKIPPED_NOTES non-empty:}
|
|
462
|
+
### Skipped Sub-Issues
|
|
463
|
+
|
|
464
|
+
${SKIPPED_NOTES}
|
|
465
|
+
|
|
466
|
+
{if owner override detected:}
|
|
467
|
+
### Owner Override
|
|
468
|
+
|
|
469
|
+
{override direction and how it changed the sub-issue scopes/order}
|
|
470
|
+
|
|
273
471
|
### Decomposition Rationale
|
|
274
472
|
|
|
275
473
|
{brief summary of why this issue was decomposed and the dependency ordering chosen}
|
|
276
474
|
${ATTRIBUTION_LINE}
|
|
277
|
-
<!-- FORGE:DECOMPOSED:COMPLETE -->"
|
|
475
|
+
<!-- FORGE:DECOMPOSED:COMPLETE -->") # allowlist:check-command-side-effects
|
|
278
476
|
```
|
|
279
477
|
|
|
280
478
|
---
|
|
281
479
|
|
|
282
480
|
## Phase D6: Update Labels
|
|
283
481
|
|
|
482
|
+
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.
|
|
483
|
+
|
|
284
484
|
```bash
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
485
|
+
run() { if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: $*"; else "$@"; fi; }
|
|
486
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
487
|
+
case "$TIER" in
|
|
488
|
+
adaptive|universal) run bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} decomposed ;;
|
|
489
|
+
prose)
|
|
490
|
+
run gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:decomposed" \
|
|
491
|
+
--remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid" 2>/dev/null || true
|
|
492
|
+
;;
|
|
493
|
+
esac
|
|
288
494
|
```
|
|
289
495
|
|
|
290
496
|
---
|
|
@@ -293,15 +499,7 @@ gh issue edit {NUMBER} {GH_FLAG} \
|
|
|
293
499
|
|
|
294
500
|
Decomposition is a terminal route for the parent issue. Each sub-issue will be picked up separately by /work-on.
|
|
295
501
|
|
|
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
|
-
```
|
|
502
|
+
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
503
|
|
|
306
504
|
**Router behavior after DECOMPOSE_RESULT**: `break` — do not continue to build/review/close for the parent issue.
|
|
307
505
|
|
|
@@ -314,7 +512,7 @@ The subcommand writes its results to GitHub (FORGE:DECOMPOSED comment + sub-issu
|
|
|
314
512
|
```
|
|
315
513
|
DECOMPOSE_RESULT:
|
|
316
514
|
status: COMPLETE | ALREADY_DONE | BLOCKED
|
|
317
|
-
sub_issues: [
|
|
515
|
+
sub_issues: [N, N, ...]
|
|
318
516
|
comment_url: {url of posted FORGE:DECOMPOSED comment}
|
|
319
517
|
blocker: {description if status=BLOCKED}
|
|
320
518
|
```
|