forgedock 1.27.0 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/bootstrap.mjs +55 -0
- package/bin/engine/outcome-codes.mjs +10 -0
- package/bin/engine/telemetry-seam.mjs +37 -8
- package/bin/engine/telemetry-seam.test.mjs +31 -0
- package/bin/engine-cli.mjs +542 -38
- package/bin/engine.mjs +8 -4
- package/bin/forgedock.mjs +75 -9
- package/bin/gc.mjs +580 -0
- package/bin/lean/admission.mjs +33 -3
- package/bin/lean/batch.mjs +22 -4
- package/bin/lean/batch.test.mjs +5 -3
- package/bin/lean/checks.mjs +199 -0
- package/bin/lean/checks.test.mjs +183 -0
- package/bin/lean/conformance.mjs +190 -58
- package/bin/lean/conformance.test.mjs +116 -7
- package/bin/lean/credentials.mjs +29 -0
- package/bin/lean/depcache.mjs +312 -0
- package/bin/lean/depcache.test.mjs +298 -0
- package/bin/lean/drift.mjs +198 -0
- package/bin/lean/drift.test.mjs +223 -0
- package/bin/lean/effects.mjs +85 -8
- package/bin/lean/execution.e2e.test.mjs +81 -0
- package/bin/lean/execution.mjs +431 -0
- package/bin/lean/execution.test.mjs +583 -0
- package/bin/lean/heavy.mjs +289 -0
- package/bin/lean/heavy.test.mjs +207 -0
- package/bin/lean/lean.mjs +572 -139
- package/bin/lean/lean.test.mjs +1193 -31
- package/bin/lean/ledger.mjs +27 -2
- package/bin/lean/ledger.test.mjs +36 -1
- package/bin/lean/limits.mjs +58 -0
- package/bin/lean/limits.test.mjs +150 -0
- package/bin/lean/orchestrate.mjs +312 -19
- package/bin/lean/prompts.mjs +89 -15
- package/bin/lean/refactor.mjs +276 -0
- package/bin/lean/refactor.test.mjs +359 -0
- package/bin/lean/render.mjs +24 -5
- package/bin/lean/review-trigger.mjs +134 -0
- package/bin/lean/review-trigger.test.mjs +104 -0
- package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
- package/bin/lean/sandbox-remote.mjs +142 -0
- package/bin/lean/sched.mjs +221 -0
- package/bin/lean/sched.test.mjs +310 -0
- package/bin/lean/schedd.mjs +247 -0
- package/bin/lean/schedd.test.mjs +180 -0
- package/bin/lean/toolchain.mjs +228 -0
- package/bin/lean/toolchain.test.mjs +154 -0
- package/bin/lean/verify.mjs +20 -9
- package/bin/lean/verify.test.mjs +26 -3
- package/bin/lean/worktree.mjs +69 -0
- package/bin/lean/worktree.test.mjs +141 -0
- package/bin/runner.mjs +53 -10
- package/bin/stats/github-source.mjs +1 -1
- package/bin/stats/index.mjs +14 -2
- package/bin/stats/query.mjs +72 -0
- package/bin/stats/stats.test.mjs +54 -0
- package/commands/audit.md +2 -0
- package/commands/autopilot.md +2 -0
- package/commands/bootstrap.md +35 -0
- package/commands/cleanup.md +7 -22
- package/commands/forge-stats.md +2 -1
- package/commands/forgedock-init.md +7 -0
- package/commands/issue.md +89 -830
- package/commands/milestone.md +55 -6
- package/commands/orchestrate.md +10 -2
- package/commands/upgrade-deps.md +3 -3
- package/commands/work-on/build/validate.md +4 -0
- package/commands/work-on/close.md +2 -0
- package/commands/work-on/decompose.md +24 -6
- package/docs/CI.md +3 -3
- package/docs/CONFIG.md +177 -2
- package/docs/OUTCOME-CODES.md +8 -0
- package/docs/TELEMETRY.md +1 -1
- package/docs/site/command-reference.md +22 -0
- package/docs/site/getting-started.md +2 -0
- package/docs/site/start-a-new-project.md +327 -0
- package/package.json +1 -1
- package/scripts/affected-files-check.sh +91 -0
- package/scripts/affected-files-check.test.sh +80 -0
- package/scripts/bootstrap-create.mjs +393 -0
- package/scripts/bootstrap-validate.mjs +36 -2
- package/scripts/calibration.mjs +116 -0
- package/scripts/check-devdocs-freshness.sh +9 -4
- package/scripts/check-devdocs-freshness.test.sh +11 -1
- package/scripts/check-spec-markers.sh +3 -0
- package/scripts/check-spec-size.sh +29 -11
- package/scripts/check-spec-size.test.sh +27 -4
- package/scripts/ci-local/lib.mjs +8 -1
- package/scripts/ci-local/parallelism.test.mjs +27 -0
- package/scripts/ci-local/run.mjs +3 -3
- package/scripts/ci-local/scoping.mjs +82 -0
- package/scripts/ci-local.test.sh +1 -1
- package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
- package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
- package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
- package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
- package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
- package/scripts/fixtures/symbol-index/py/sample.py +41 -0
- package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
- package/scripts/fixtures/symbol-move/gen_big.py +201 -0
- package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
- package/scripts/forge-root.test.sh +1 -1
- package/scripts/hotspots.sh +100 -0
- package/scripts/hotspots.test.sh +101 -0
- package/scripts/issue-create.sh +197 -0
- package/scripts/issue-create.test.sh +166 -0
- package/scripts/maintenance-scan.sh +185 -0
- package/scripts/maintenance-scan.test.sh +89 -0
- package/scripts/milestone-drift.sh +206 -0
- package/scripts/milestone-drift.test.sh +120 -0
- package/scripts/refactor-verify.sh +368 -0
- package/scripts/refactor-verify.test.sh +202 -0
- package/scripts/structure-delta.sh +10 -5
- package/scripts/structure-delta.test.sh +3 -2
- package/scripts/symbol-index.mjs +253 -0
- package/scripts/symbol-index.py +354 -0
- package/scripts/symbol-index.test.sh +119 -0
- package/scripts/symbol-move-ts.test.sh +161 -0
- package/scripts/symbol-move.mjs +379 -0
- package/scripts/symbol-move.py +383 -0
- package/scripts/symbol-move.test.sh +168 -0
- package/templates/bootstrap/interview.md +34 -0
- package/templates/bootstrap/issue-body.md +42 -0
- package/templates/bootstrap/needs-human-issue.md +45 -0
- package/templates/bootstrap/stack-profiles.md +34 -0
- package/templates/bootstrap/technical-spike.md +75 -0
package/commands/issue.md
CHANGED
|
@@ -11,182 +11,51 @@ argument-hint: "[description of the problem or feature] [--dry-run] | --title \"
|
|
|
11
11
|
|
|
12
12
|
**Input**: $ARGUMENTS
|
|
13
13
|
|
|
14
|
-
You create GitHub issues with the
|
|
14
|
+
You create GitHub issues with the structure the `/work-on` pipeline expects. Every deterministic step (argument parsing, body-file integrity, dedup, mandatory sections, REST create with token read-back, milestone, project board) runs in `scripts/issue-create.sh`. This spec holds only routing and the drafting judgment. Issues are created by default once the checks pass; `--dry-run` prints the draft instead.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" --body-file \"$BODY_FILE\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — every `--body-file` caller must use an entity- and agent-scoped scratch path and include a unique integrity marker which `/issue` verifies after creation. This is required because a collision can substitute plausible but unrelated content, not just produce an obvious file error (forge#2843).
|
|
19
|
-
|
|
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.
|
|
16
|
+
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet". Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
|
|
21
17
|
**NEVER use plan mode (EnterPlanMode).**
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
## Argument Parsing
|
|
19
|
+
## Arguments
|
|
26
20
|
|
|
27
21
|
| Flag | Effect |
|
|
28
22
|
|------|--------|
|
|
29
|
-
| (none) |
|
|
30
|
-
| `--dry-run` |
|
|
31
|
-
| `--
|
|
32
|
-
| `--
|
|
33
|
-
| `--
|
|
34
|
-
| `--
|
|
35
|
-
| `--
|
|
36
|
-
| `--
|
|
23
|
+
| (none) | Interactive: read code, draft, then create through the script (below). |
|
|
24
|
+
| `--dry-run` | Validate and print the draft; create nothing. Never bypasses the dedup STOP. |
|
|
25
|
+
| `--greenfield` | Affected Files may be *planned* paths, validated against the spike doc's declared repo layout (Validate, below). Also on automatically when the spike doc has a `Repo layout` heading. <!-- Added: forge#4172 --> |
|
|
26
|
+
| `--spike <path>` | Spike doc for greenfield mode (implies `--greenfield`). Default `docs/engineering/technical-spike.md`, repo-relative. |
|
|
27
|
+
| `--title "..."` | **The sole programmatic-mode trigger.** Final title; skips drafting. |
|
|
28
|
+
| `--body "..."` / `--body-file <path>` | Final body; exactly one is required with `--title`. |
|
|
29
|
+
| `--label "..."` | One label; repeatable. |
|
|
30
|
+
| `--milestone "..."` | Milestone title (optional). |
|
|
31
|
+
| `--exclude "N,N,..."` | Issue numbers removed from the dedup candidate set (a deliberate supersede, e.g. a P3 batch issue restating its members). Narrows the set; NOT a `--force` equivalent. |
|
|
32
|
+
|
|
33
|
+
## Programmatic Mode (`--title` present)
|
|
34
|
+
|
|
35
|
+
Run exactly one call and relay its result. Do not read code, draft, or re-run any check yourself.
|
|
37
36
|
|
|
38
37
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
PROGRAMMATIC_LABELS=()
|
|
44
|
-
PROGRAMMATIC_MILESTONE=""
|
|
45
|
-
PROGRAMMATIC_EXCLUDE=""
|
|
46
|
-
|
|
47
|
-
# Positional/flag parsing. $ARGUMENTS is a single opaque string (the invoking
|
|
48
|
-
# agent's raw args, e.g. from `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" ...")`)
|
|
49
|
-
# — it is NOT pre-tokenized by any harness, and a naive `ARGS=($ARGUMENTS)` would
|
|
50
|
-
# word-split on whitespace without honoring embedded quote characters (a literal
|
|
51
|
-
# `"` inside the string is not shell syntax at this point), fragmenting any
|
|
52
|
-
# multi-word `--title`/`--body` value (forge#2094).
|
|
53
|
-
#
|
|
54
|
-
# Do NOT tokenize via `eval "set -- $ARGUMENTS"`. An earlier fix for #2094 used
|
|
55
|
-
# exactly that — it is quote-aware, but `eval` re-parses and EXECUTES the full
|
|
56
|
-
# substituted string as shell code: any unescaped `$(...)`, backtick, `;`, `|`,
|
|
57
|
-
# or `&&` embedded in a caller-supplied title/body value runs as a command
|
|
58
|
-
# during parsing, not just as text. `/issue` is invoked programmatically by
|
|
59
|
-
# other automation (audit tools, /work-on decomposition, review-finding
|
|
60
|
-
# creation — see commands/work-on.md, commands/review-pr.md,
|
|
61
|
-
# commands/review-pr-staging.md, commands/test-gate.md) with less-trusted
|
|
62
|
-
# strings, so this is a real call-site injection surface, not a theoretical one.
|
|
63
|
-
#
|
|
64
|
-
# Use `xargs` instead: it applies the same quote/backslash-aware word-splitting
|
|
65
|
-
# a shell command line would (so `--title "fix: billing crash"` still yields
|
|
66
|
-
# ONE token), but — unlike `eval` — it never expands `$(...)`, never expands
|
|
67
|
-
# backticks, and never executes anything. NUL-delimit the output (`printf
|
|
68
|
-
# '%s\0'` / `xargs -n1 ... -0`) so tokens containing embedded newlines still
|
|
69
|
-
# round-trip correctly through `mapfile -d ''`.
|
|
70
|
-
#
|
|
71
|
-
# `mapfile -d ''` requires bash >= 4.4 — consistent with the minimum this
|
|
72
|
-
# pipeline already assumes elsewhere (e.g. `resolve_script()`'s tier-dispatch
|
|
73
|
-
# in work-on.md), so this is not a new constraint.
|
|
74
|
-
#
|
|
75
|
-
# Do NOT silence xargs's exit status with `2>/dev/null`: xargs has a
|
|
76
|
-
# platform-dependent max single-argument length (POSIX guarantees at least
|
|
77
|
-
# 4096 bytes; Linux/macOS observed limits are far higher but not unbounded).
|
|
78
|
-
# A token exceeding that ceiling makes xargs fail loudly — suppressing stderr
|
|
79
|
-
# would turn that into a silent truncation instead, which is worse than the
|
|
80
|
-
# word-splitting bug this whole block exists to fix. `--title`/`--body` are
|
|
81
|
-
# short by convention; `--body-file` (the mechanism every current caller
|
|
82
|
-
# actually uses for large content — see commands/work-on.md,
|
|
83
|
-
# commands/review-pr.md, commands/review-pr-staging.md, commands/test-gate.md,
|
|
84
|
-
# commands/validate.md) is unaffected, since only the file *path* passes
|
|
85
|
-
# through this tokenizer, not the file's contents.
|
|
86
|
-
#
|
|
87
|
-
# `mapfile -d '' -t ARGS < <(cmd)` cannot observe `cmd`'s exit status —
|
|
88
|
-
# process substitution runs in a detached subshell, and mapfile's own exit
|
|
89
|
-
# status only reflects whether the read succeeded, not the upstream command
|
|
90
|
-
# (verified: `mapfile -d '' -t A < <(false; printf 'x\0')` exits 0). Route
|
|
91
|
-
# through a temp file instead so the real xargs exit status is captured.
|
|
92
|
-
ARGS_TMP=$(mktemp)
|
|
93
|
-
if ! printf '%s' "$ARGUMENTS" | xargs -n1 printf '%s\0' > "$ARGS_TMP"; then
|
|
94
|
-
echo "ERROR: failed to tokenize \$ARGUMENTS — a single token may exceed xargs's max argument length. Use --body-file for large content instead of inline --body." >&2
|
|
95
|
-
rm -f "$ARGS_TMP"
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
mapfile -d '' -t ARGS < "$ARGS_TMP"
|
|
99
|
-
rm -f "$ARGS_TMP"
|
|
100
|
-
|
|
101
|
-
# Flag-mode gate: only run the flag-parsing loop (and its hard-error on
|
|
102
|
-
# unrecognized --flags below) when at least one token EXACTLY matches a
|
|
103
|
-
# recognized flag. Free-text invocations (the "(none)" row above) are plain
|
|
104
|
-
# English descriptions and may legitimately contain a "--something" substring
|
|
105
|
-
# as part of the prose (e.g. "fix: crash when using --verbose flag") — those
|
|
106
|
-
# must fall through untouched to Phase 1's free-text parser, not be treated
|
|
107
|
-
# as a caller typo. Without this gate, the --*) error arm below would abort
|
|
108
|
-
# on any free-text description that happens to mention a flag name. See
|
|
109
|
-
# forge#2096 (loop content) for why unrecognized flags must fail loudly once
|
|
110
|
-
# we ARE in flag mode.
|
|
111
|
-
LOOKS_LIKE_FLAG_MODE=false
|
|
112
|
-
for arg in "${ARGS[@]}"; do
|
|
113
|
-
case "$arg" in
|
|
114
|
-
--dry-run|--title|--body|--body-file|--label|--milestone|--exclude)
|
|
115
|
-
LOOKS_LIKE_FLAG_MODE=true
|
|
116
|
-
break
|
|
117
|
-
;;
|
|
118
|
-
esac
|
|
119
|
-
done
|
|
120
|
-
|
|
121
|
-
i=0
|
|
122
|
-
while [ "$LOOKS_LIKE_FLAG_MODE" = "true" ] && [ $i -lt ${#ARGS[@]} ]; do
|
|
123
|
-
arg="${ARGS[$i]}"
|
|
124
|
-
case "$arg" in
|
|
125
|
-
--dry-run) DRY_RUN=true ;;
|
|
126
|
-
--title|--body|--body-file|--label|--milestone|--exclude)
|
|
127
|
-
# Each of these flags requires a value token immediately after it. A flag
|
|
128
|
-
# with no following token (e.g. --title as the last argument) is a usage
|
|
129
|
-
# error, not an empty value — fail loudly instead of silently proceeding
|
|
130
|
-
# with PROGRAMMATIC_TITLE="" (which would otherwise mis-detect as
|
|
131
|
-
# non-programmatic mode and mask the caller's mistake).
|
|
132
|
-
if [ $((i+1)) -ge ${#ARGS[@]} ]; then
|
|
133
|
-
echo "ERROR: $arg requires a value"
|
|
134
|
-
exit 1
|
|
135
|
-
fi
|
|
136
|
-
i=$((i+1))
|
|
137
|
-
case "$arg" in
|
|
138
|
-
--title) PROGRAMMATIC_TITLE="${ARGS[$i]}" ;;
|
|
139
|
-
--body) PROGRAMMATIC_BODY="${ARGS[$i]}" ;;
|
|
140
|
-
--body-file) PROGRAMMATIC_BODY_FILE="${ARGS[$i]}" ;;
|
|
141
|
-
--label) PROGRAMMATIC_LABELS+=("${ARGS[$i]}") ;;
|
|
142
|
-
--milestone) PROGRAMMATIC_MILESTONE="${ARGS[$i]}" ;;
|
|
143
|
-
--exclude) PROGRAMMATIC_EXCLUDE="${ARGS[$i]}" ;;
|
|
144
|
-
esac
|
|
145
|
-
;;
|
|
146
|
-
--*)
|
|
147
|
-
# Unrecognized flag (e.g. a mistyped --titel instead of --title). Fail
|
|
148
|
-
# loudly instead of silently discarding it — a silently-ignored flag
|
|
149
|
-
# can fail open into the interactive/free-text path (PROGRAMMATIC_MODE
|
|
150
|
-
# stays false because PROGRAMMATIC_TITLE never gets set) with no
|
|
151
|
-
# diagnostic pointing at the caller's typo. See forge#2096.
|
|
152
|
-
#
|
|
153
|
-
# Deliberately NOT a bare `*)` catch-all: free-text invocations (the
|
|
154
|
-
# "(none)" row in the Argument Parsing table above, e.g. "the billing
|
|
155
|
-
# page crashes when credits hit zero") tokenize into plain words with
|
|
156
|
-
# no leading `--`, and those words are intentionally NOT flags — they
|
|
157
|
-
# are consumed later via $ARGUMENTS as a whole by Phase 1's free-text
|
|
158
|
-
# parser. Only tokens that look like a flag (start with `--`) but
|
|
159
|
-
# don't match a known one are a caller mistake worth failing loudly on.
|
|
160
|
-
echo "ERROR: unrecognized flag: $arg" >&2
|
|
161
|
-
exit 1
|
|
162
|
-
;;
|
|
163
|
-
esac
|
|
164
|
-
i=$((i+1))
|
|
165
|
-
done
|
|
166
|
-
|
|
167
|
-
# PROGRAMMATIC_MODE is true iff --title was supplied. Every other structured flag
|
|
168
|
-
# (--body/--body-file/--label/--milestone) is meaningless without a title and does
|
|
169
|
-
# NOT, on its own, activate programmatic mode — this avoids a caller accidentally
|
|
170
|
-
# triggering the skip-Phase-1/3 path with a partial/malformed flag set.
|
|
171
|
-
if [ -n "$PROGRAMMATIC_TITLE" ]; then
|
|
172
|
-
PROGRAMMATIC_MODE=true
|
|
173
|
-
else
|
|
174
|
-
PROGRAMMATIC_MODE=false
|
|
175
|
-
fi
|
|
38
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
39
|
+
ISSUE_CREATE_SCRIPT=$(forge_resolve issue-create.sh)
|
|
40
|
+
[ -n "$ISSUE_CREATE_SCRIPT" ] || { echo "ERROR: issue-create.sh not found in a trusted ForgeDock install" >&2; echo "ISSUE_CREATE_RESULT:ERROR"; exit 3; }
|
|
41
|
+
bash "$ISSUE_CREATE_SCRIPT" --args "$ARGUMENTS"
|
|
176
42
|
```
|
|
177
43
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
### Programmatic Invocation Contract
|
|
44
|
+
The script prints exactly one result line; report it verbatim and exit with its code:
|
|
181
45
|
|
|
182
|
-
|
|
46
|
+
| Exit | Line | Meaning |
|
|
47
|
+
|------|------|---------|
|
|
48
|
+
| 0 | `ISSUE_CREATE_RESULT:CREATED number=N url=U` | Created; the create-token read-back (and, for `--body-file`, the integrity marker read-back) passed |
|
|
49
|
+
| 0 | `ISSUE_CREATE_RESULT:DRY_RUN` | `--dry-run`: draft printed, nothing created |
|
|
50
|
+
| 1 | `ISSUE_CREATE_RESULT:DEDUP number=N` | Open near-duplicate #N; nothing created. STOP |
|
|
51
|
+
| 2 | `ISSUE_CREATE_RESULT:USAGE` | Bad flags, body, integrity marker or dedup invocation; fix the call |
|
|
52
|
+
| 3 | `ISSUE_CREATE_RESULT:ERROR` | GitHub rejected the create or a read-back failed; never report success |
|
|
183
53
|
|
|
184
|
-
|
|
185
|
-
**Optional**: `--label "NAME"` (repeatable, zero or more), `--milestone "TITLE"`, `--dry-run`, `--exclude "N,N,..."` (see Phase 2D — narrows the dedup candidate set, not a `--force` equivalent).
|
|
54
|
+
The repo comes from `forge.yaml` (`project.owner/project.repo`); pass `--repo OWNER/NAME` outside `--args` for a satellite repo. Missing `## Problem`, `## Affected Files` or `## Acceptance Criteria` sections get placeholders, never a failure.
|
|
186
55
|
|
|
187
|
-
|
|
56
|
+
### `--body-file` integrity contract (MANDATORY for the caller)
|
|
188
57
|
|
|
189
|
-
|
|
58
|
+
Do not use a generic name under shared `/tmp`, even with `mktemp`. Prefer the session scratchpad (or a repo-relative scratch directory): a Windows-native `gh` may not resolve Git Bash `/tmp`. Never hand-roll a temporary path or use a single-segment root path such as `/tmp_invbody_31076.txt`, which can require interactive deletion approval and hang unattended runs. Make the filename include the target issue or PR number and an agent-unique token, and let `mktemp` add the random suffix:
|
|
190
59
|
|
|
191
60
|
```bash
|
|
192
61
|
SCRATCHPAD="${FORGE_SCRATCHPAD:-$PWD/.forge-scratch}"
|
|
@@ -197,715 +66,105 @@ BODY_FILE=$(mktemp "$SCRATCHPAD/${PR_NUMBER}_review_${AGENT_TOKEN}.XXXXXX.md")
|
|
|
197
66
|
printf '%s\n<!-- %s -->\n' "$BODY" "$BODY_MARKER" > "$BODY_FILE"
|
|
198
67
|
```
|
|
199
68
|
|
|
200
|
-
The body MUST contain exactly one
|
|
201
|
-
|
|
202
|
-
**Fail-loud read and verification**: `/issue` reads `$PROGRAMMATIC_BODY_FILE` once into memory below. For `--body-file`, it also extracts the required marker and, after `gh issue create`, re-reads the created issue and requires that exact marker. Missing, unreadable, absent, or ambiguous markers are hard errors; do not create or report success with unverified content.
|
|
203
|
-
|
|
204
|
-
```bash
|
|
205
|
-
if [ "$PROGRAMMATIC_MODE" = "true" ]; then
|
|
206
|
-
if [ -n "$PROGRAMMATIC_BODY" ] && [ -n "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
207
|
-
echo "ERROR: programmatic mode requires exactly one of --body or --body-file (both given)"
|
|
208
|
-
exit 1
|
|
209
|
-
elif [ -z "$PROGRAMMATIC_BODY" ] && [ -z "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
210
|
-
echo "ERROR: programmatic mode requires exactly one of --body or --body-file (neither given)"
|
|
211
|
-
exit 1
|
|
212
|
-
elif [ -n "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
213
|
-
# Fail loudly on a missing/unreadable file — do NOT let a failed `cat` fall
|
|
214
|
-
# through as an empty $PROGRAMMATIC_BODY. An empty body would otherwise look
|
|
215
|
-
# like "no body given" to Phase 3F below, which would silently repair it
|
|
216
|
-
# with placeholder-only stub sections instead of surfacing the caller's
|
|
217
|
-
# file-path mistake as an error, same fail-loudly convention as the
|
|
218
|
-
# both/neither check directly above.
|
|
219
|
-
if [ ! -r "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
220
|
-
echo "ERROR: --body-file '$PROGRAMMATIC_BODY_FILE' is missing or unreadable"
|
|
221
|
-
exit 1
|
|
222
|
-
fi
|
|
223
|
-
PROGRAMMATIC_BODY=$(cat "$PROGRAMMATIC_BODY_FILE")
|
|
224
|
-
mapfile -t PROGRAMMATIC_BODY_MARKERS < <(printf '%s' "$PROGRAMMATIC_BODY" | grep -oE '<!-- FORGE:BODY-INTEGRITY:[^>]+ -->')
|
|
225
|
-
if [ "${#PROGRAMMATIC_BODY_MARKERS[@]}" -ne 1 ]; then
|
|
226
|
-
echo "ERROR: --body-file must contain exactly one FORGE:BODY-INTEGRITY marker"
|
|
227
|
-
exit 1
|
|
228
|
-
fi
|
|
229
|
-
PROGRAMMATIC_BODY_MARKER="${PROGRAMMATIC_BODY_MARKERS[0]}"
|
|
230
|
-
fi
|
|
231
|
-
fi
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Routing** (see also Phase 1's routing guard below): when `PROGRAMMATIC_MODE=true`, skip directly from here to **Phase 2D** using `{PROPOSED_TITLE}` = `$PROGRAMMATIC_TITLE` — Phase 1 (parse) and Phase 3 (draft) do not run. After Phase 2D passes, run **Phase 3F: Programmatic Pre-Create Validation** (below), then proceed to the unmodified Phase 4 (create). `{PRIORITY_LABEL},{CATEGORY_LABEL}` in Phase 4B is the comma-join of `PROGRAMMATIC_LABELS[@]`; the milestone flag is included iff `PROGRAMMATIC_MILESTONE` is non-empty.
|
|
235
|
-
|
|
236
|
-
The free-text/interactive path (`PROGRAMMATIC_MODE=false`) is completely unaffected — it always proceeds through Phase 1 → Phase 2 → Phase 3 → Phase 4 exactly as before this contract was added.
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Why This Exists
|
|
241
|
-
|
|
242
|
-
Bad issues cause cascading pipeline failures. When an issue says "fix the docker mount" without specifying WHICH compose file, the investigator checks one file, misses the override, the builder writes an incomplete fix, the deploy fails, and a new audit issue gets filed about the original issue. One vague sentence costs 3+ agent runs. This command prevents that by enforcing structure at creation time.
|
|
243
|
-
|
|
244
|
-
---
|
|
69
|
+
The body MUST contain exactly one marker `<!-- FORGE:BODY-INTEGRITY:<entity>_<role>_<agent-token> -->` on its own line. The script refuses a body with zero or several markers, and after creation re-reads the issue and requires that exact line: a path collision can substitute another agent's plausible-looking body (forge#2843).
|
|
245
70
|
|
|
246
|
-
##
|
|
71
|
+
## Interactive Mode (free text)
|
|
247
72
|
|
|
248
|
-
|
|
73
|
+
### 1. Parse the request
|
|
249
74
|
|
|
250
|
-
|
|
75
|
+
- **Repo**: default from `forge.yaml`; a `{satellite_prefix}:` prefix routes to `repos.satellites[].repo` (pass it as `--repo`).
|
|
76
|
+
- **Type → title prefix**: bug `fix:` (crash, broken, error, regression) · feature `feat:` · refactor `refactor:` · investigation `investigate:` · infra `fix:`/`feat:` · docs `docs:`.
|
|
77
|
+
- **Priority**: P0 prod down, data loss, security · P1 significant user impact, deploy blocked · P2 minor bug or improvement (default) · P3 cosmetic. Never guess P0/P1; ask.
|
|
78
|
+
- **Category** (one): `bug`, `feature`, `enhancement`, `refactor`, `dead-code`, `infra`, `docs`, `performance`, `seo`, `ux`, `frontend`, `audit-finding`.
|
|
251
79
|
|
|
252
|
-
|
|
253
|
-
|-----------|---------|
|
|
254
|
-
| Free-text description | "the billing page crashes when credits hit zero" |
|
|
255
|
-
| URL to error/log | "https://sentry.io/..." or a pasted stack trace |
|
|
256
|
-
| Reference to code | "the background worker leaks memory in task_processor.py" |
|
|
257
|
-
| Feature request | "add a dark mode toggle to the dashboard settings page" |
|
|
258
|
-
| Multi-repo prefix | "mcp: add list_schemas tool" or "n8n: fix credential refresh" |
|
|
259
|
-
| Audit/investigation | "investigate why deploy times doubled this week" |
|
|
80
|
+
### 2. Gather context (MANDATORY)
|
|
260
81
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
Read `forge.yaml` to build the routing table dynamically:
|
|
264
|
-
|
|
265
|
-
```bash
|
|
266
|
-
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
267
|
-
GH_REPO=$(yq '.project.owner + "/" + .project.repo' "$CONFIG_FILE")
|
|
268
|
-
GH_FLAG="-R $GH_REPO"
|
|
269
|
-
REPO_PATH=$(yq '.paths.root' "$CONFIG_FILE")
|
|
270
|
-
STAGING_BRANCH=$(yq '.branches.staging' "$CONFIG_FILE")
|
|
271
|
-
# Satellite repos from forge.yaml → repos.satellites (each has: prefix, repo, staging_branch)
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
| Prefix | Repository | GH_REPO | GH_FLAG |
|
|
275
|
-
|--------|-----------|---------|---------|
|
|
276
|
-
| (none) | Default project | `{GH_REPO}` from `forge.yaml → project.owner/repo` | (none / use `{GH_FLAG}`) |
|
|
277
|
-
| `{satellite_prefix}:` | Satellite repo | `{satellite.repo}` from `forge.yaml → repos.satellites` | `-R {satellite.repo}` |
|
|
278
|
-
|
|
279
|
-
### 1B: Classify issue type
|
|
280
|
-
|
|
281
|
-
| Type | Signals | Title Prefix |
|
|
282
|
-
|------|---------|-------------|
|
|
283
|
-
| **Bug** | "crash", "broken", "fails", "error", stack trace, regression | `fix:` |
|
|
284
|
-
| **Feature** | "add", "enable", "support", "new", "implement" | `feat:` |
|
|
285
|
-
| **Refactor** | "clean up", "simplify", "extract", "rename", "dead code" | `refactor:` |
|
|
286
|
-
| **Investigation** | "investigate", "audit", "research", "evaluate", "why does" | `investigate:` |
|
|
287
|
-
| **Infra** | "deploy", "CI", "docker", "workflow", "pipeline" | `fix:` or `feat:` |
|
|
288
|
-
| **Docs** | "document", "README", "guide" | `docs:` |
|
|
289
|
-
|
|
290
|
-
### 1C: Classify priority
|
|
291
|
-
|
|
292
|
-
| Priority | Signals |
|
|
293
|
-
|----------|---------|
|
|
294
|
-
| **P0** | Production down, data loss, security vulnerability, user-facing crash |
|
|
295
|
-
| **P1** | Significant bug affecting users, broken feature, deploy blocked |
|
|
296
|
-
| **P2** | Minor bug, improvement, non-critical enhancement |
|
|
297
|
-
| **P3** | Cosmetic, nice-to-have, low-impact cleanup |
|
|
298
|
-
|
|
299
|
-
Default to **P2** if unclear. Never guess P0/P1 — ask the user if it seems critical but they didn't say so.
|
|
300
|
-
|
|
301
|
-
### 1D: Determine category label
|
|
302
|
-
|
|
303
|
-
Pick ONE primary category label:
|
|
304
|
-
- `bug` — something is broken
|
|
305
|
-
- `feature` — new capability
|
|
306
|
-
- `enhancement` — improvement to existing feature
|
|
307
|
-
- `refactor` — code restructuring, no behavior change
|
|
308
|
-
- `dead-code` — removing unused code
|
|
309
|
-
- `infra` — CI/CD, Docker, deployment
|
|
310
|
-
- `docs` — documentation
|
|
311
|
-
- `performance` — speed/resource optimization
|
|
312
|
-
- `seo` — search engine optimization
|
|
313
|
-
- `ux` — user experience improvement
|
|
314
|
-
- `frontend` — frontend-specific change
|
|
315
|
-
- `audit-finding` — discovered during pipeline audit
|
|
316
|
-
|
|
317
|
-
---
|
|
82
|
+
Read the actual code before drafting: the investigator inherits your file references. Map the domain (`forge.yaml → review.context`, the project's `CLAUDE.md`), read files from `origin/{STAGING_BRANCH}` via `git show`, and list every file the change touches: the primary file, its tests, config layers (`.env.example`, every compose override), migrations for schema changes, importers of a changed function.
|
|
318
83
|
|
|
319
|
-
|
|
84
|
+
Duplicates: the script's token-overlap dedup is authoritative. Before drafting, also run a semantic search (`gh issue list --state open --search "{key_terms}"`, and `--state closed` for regressions). An open duplicate: tell the user and do not create. A closed one: create, and write "Regression of #N." in the body. If the user explicitly overrides a dedup STOP, re-run with `--exclude <that number>`; agents never do this without the user's authorization.
|
|
320
85
|
|
|
321
|
-
|
|
86
|
+
### 3. Draft
|
|
322
87
|
|
|
323
|
-
**
|
|
88
|
+
**Title**: `{prefix}({scope}): {specific description}`, max 80 characters, no vague verbs ("improve", "update", "handle") without specifics.
|
|
324
89
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
Map the issue to its domain and key files. Domain structure comes from the project — check `forge.yaml → review.context` and the project's `CLAUDE.md` for project-specific domain maps.
|
|
328
|
-
|
|
329
|
-
For ForgeDock issues (this repo): `commands/*.md`, `CLAUDE.md`, `install.sh`, `bin/`
|
|
330
|
-
For satellite repo issues: read the satellite repo's source directory (from `forge.yaml → repos.satellites`)
|
|
331
|
-
|
|
332
|
-
### 2B: Read the affected files
|
|
333
|
-
|
|
334
|
-
**CRITICAL RULE**: Read the ACTUAL code that the issue is about. Not just the file the user mentioned — also:
|
|
335
|
-
|
|
336
|
-
1. **Override/companion files**: If the issue mentions `docker-compose.yml`, ALSO check `docker-compose.prod.yml`. If it mentions a router, ALSO check the model it imports from.
|
|
337
|
-
2. **Config layers**: If the issue is about environment variables, check `.env.example`, `docker-compose.yml`, AND `docker-compose.prod.yml`.
|
|
338
|
-
3. **Import chains**: If the issue is about a function, check what imports it and what it imports.
|
|
339
|
-
|
|
340
|
-
```bash
|
|
341
|
-
# Resolve the right branch to read from
|
|
342
|
-
# Check the staging branch first (most recent deployable state), then the default branch
|
|
343
|
-
git fetch origin {STAGING_BRANCH} {DEFAULT_BRANCH} 2>/dev/null
|
|
344
|
-
|
|
345
|
-
# Read the files relevant to the issue
|
|
346
|
-
# Use git show origin/{STAGING_BRANCH}:{filepath} for project files
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
### 2C: Identify ALL affected files
|
|
350
|
-
|
|
351
|
-
List every file that the fix/feature will need to touch. Be thorough:
|
|
352
|
-
- The primary file with the bug/feature location
|
|
353
|
-
- Test files that cover the affected code
|
|
354
|
-
- Config files that need updating
|
|
355
|
-
- Migration files if schema changes are needed
|
|
356
|
-
- Frontend files if the change has UI implications
|
|
357
|
-
- **Override files** (e.g., `docker-compose.prod.yml` if `docker-compose.yml` is affected)
|
|
358
|
-
|
|
359
|
-
### 2D: Check for duplicates
|
|
360
|
-
|
|
361
|
-
Run the deterministic dedup script first (authoritative check), then fall back to an LLM pass if the script produces no result:
|
|
362
|
-
|
|
363
|
-
```bash
|
|
364
|
-
# Authoritative deterministic check — uses token overlap algorithm (see scripts/issue-dedup.sh)
|
|
365
|
-
# In programmatic mode, forward $PROGRAMMATIC_EXCLUDE (if set) as --exclude — see the
|
|
366
|
-
# "Exclusion mechanism" note below. In interactive mode $PROGRAMMATIC_EXCLUDE is always
|
|
367
|
-
# empty, so this is a no-op for that path.
|
|
368
|
-
DEDUP_EXCLUDE_ARGS=()
|
|
369
|
-
[ -n "$PROGRAMMATIC_EXCLUDE" ] && DEDUP_EXCLUDE_ARGS=(--exclude "$PROGRAMMATIC_EXCLUDE")
|
|
370
|
-
# HELPER_SCRIPT resolver (DEDUP_SCRIPT; trusted-install tiers only, never the working directory or the target repo, #3400/#3483): plugin root, FORGE_ROOT, FORGEDOCK_HOME, FORGE_HOME, install symlink, marketplaces, newest pinned plugin cache under CLAUDE_CONFIG_DIR then ~/.claude. The cache scan matters because the plugin-root placeholder is not always substituted in forked runs.
|
|
371
|
-
_l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
|
|
372
|
-
_tc="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l" "$HOME/.claude/plugins/marketplaces/forgedock")"
|
|
373
|
-
for _cfg in "${CLAUDE_CONFIG_DIR:-}" "$HOME/.claude"; do
|
|
374
|
-
[ -n "$_cfg" ] && _tc="$_tc"$'\n'"$(find -L "$_cfg/plugins/cache/forgedock/forgedock" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | awk -F/ '$NF ~ /^[0-9]+\.[0-9]+\.[0-9]+$/{split($NF,a,".");printf "%d %d %d %s\n",a[1],a[2],a[3],$(0)}' | sort -k1,1nr -k2,2nr -k3,3nr | cut -d' ' -f4- || true)"
|
|
375
|
-
done
|
|
376
|
-
DEDUP_SCRIPT=""
|
|
377
|
-
while IFS= read -r _c; do
|
|
378
|
-
case "$_c" in /*) [ -z "$DEDUP_SCRIPT" ] && [ -f "$_c/scripts/issue-dedup.sh" ] && DEDUP_SCRIPT="$_c/scripts/issue-dedup.sh" ;; esac
|
|
379
|
-
done <<< "$_tc"
|
|
380
|
-
# Unresolved helper: no deterministic result (exit 0, empty) so the LLM fallback below still runs; never exit 1 (duplicate STOP).
|
|
381
|
-
if [ -n "$DEDUP_SCRIPT" ]; then
|
|
382
|
-
DEDUP_RESULT=$(bash "$DEDUP_SCRIPT" "{PROPOSED_TITLE}" {GH_FLAG} "${DEDUP_EXCLUDE_ARGS[@]}" 2>&1)
|
|
383
|
-
DEDUP_EXIT=$?
|
|
384
|
-
else
|
|
385
|
-
DEDUP_RESULT=""; DEDUP_EXIT=0
|
|
386
|
-
fi
|
|
387
|
-
|
|
388
|
-
if [ "$DEDUP_EXIT" -eq 1 ]; then
|
|
389
|
-
echo "Near-duplicate detected: $DEDUP_RESULT"
|
|
390
|
-
echo "Existing issue found — do NOT create a new one."
|
|
391
|
-
DEDUP_NUMBER=$(printf '%s\n' "$DEDUP_RESULT" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
|
|
392
|
-
[ -n "$DEDUP_NUMBER" ] || { echo "ERROR: dedup STOP returned no issue number." >&2; exit 2; }
|
|
393
|
-
echo "ISSUE_CREATE_RESULT:DEDUP number=${DEDUP_NUMBER}"
|
|
394
|
-
exit 1 # STOP here and report the explicit dedup result to the caller
|
|
395
|
-
elif [ "$DEDUP_EXIT" -eq 2 ]; then
|
|
396
|
-
echo "Dedup check usage error: $DEDUP_RESULT"
|
|
397
|
-
echo "Do NOT proceed to issue creation — fix the invocation and retry."
|
|
398
|
-
exit 2 # STOP here — do not fall through to issue creation
|
|
399
|
-
fi
|
|
400
|
-
```
|
|
401
|
-
|
|
402
|
-
If the script exits 0 (no match found), also run an LLM-side search as a fallback:
|
|
403
|
-
|
|
404
|
-
```bash
|
|
405
|
-
# LLM fallback — broader semantic search for issues the token algorithm may miss
|
|
406
|
-
gh issue list {GH_FLAG} --state open --limit 20 --search "{key_terms}" --json number,title,labels --jq '.[] | "#\(.number) [\(.labels | map(.name) | join(","))] \(.title)"'
|
|
407
|
-
|
|
408
|
-
# Also check recently closed issues (might be a regression)
|
|
409
|
-
gh issue list {GH_FLAG} --state closed --limit 10 --search "{key_terms}" --json number,title,state --jq '.[] | "#\(.number) [closed] \(.title)"'
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
If a duplicate exists:
|
|
413
|
-
- **Open duplicate found**: Tell the user. Do NOT create the issue. Show the existing issue number.
|
|
414
|
-
- **Closed duplicate found (regression)**: Create the issue but reference the prior issue in the body: "Regression of #{N}."
|
|
415
|
-
- **User wants to override**: Pass `--force` to the dedup script — this is an explicit human override path. Agents MUST NOT pass `--force` without user authorization. <!-- Added: forge#1335 -->
|
|
416
|
-
|
|
417
|
-
**Exclusion mechanism (`--exclude`, programmatic mode only)**: A caller that already knows a set of open issue numbers this new issue is a deliberate supersede of — not an accidental duplicate — can pass `--exclude "N,N,..."` to `/issue` (forwarded to `scripts/issue-dedup.sh --exclude`). The canonical example is `phase-1-resolve.md`'s P3 review-finding batching: a batch issue restates its own member findings' subject matter by construction, so without exclusion it always collides with them under the token-overlap algorithm. `--exclude` removes those specific issue numbers from Phase 2D's candidate set BEFORE matching runs — it is **NOT a `--force` equivalent**: it narrows the candidate set, it does not disable the check, and it requires no human authorization (unlike `--force`, which agents MUST NOT pass without one). Any OTHER open issue — one not in the exclusion list — is still matched normally, so a title that happens to duplicate an unrelated existing issue still trips the STOP. <!-- Added: forge#2432 -->
|
|
418
|
-
|
|
419
|
-
### 2E: Check for milestone context
|
|
420
|
-
|
|
421
|
-
```bash
|
|
422
|
-
# If user mentioned a milestone or the issue clearly belongs to one:
|
|
423
|
-
gh api repos/{GH_REPO}/milestones --jq '.[] | select(.state == "open") | "#\(.number) \(.title) (\(.open_issues) open)"'
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
**Skip in programmatic mode** — `$PROGRAMMATIC_MILESTONE` (if supplied) is used directly by Phase 4B; there is nothing to look up.
|
|
427
|
-
|
|
428
|
-
---
|
|
429
|
-
|
|
430
|
-
## Phase 3: Draft the Issue
|
|
431
|
-
|
|
432
|
-
### 3A: Compose the title
|
|
433
|
-
|
|
434
|
-
Format: `{prefix}: {concise description}`
|
|
435
|
-
|
|
436
|
-
Rules:
|
|
437
|
-
- **Max 80 characters**
|
|
438
|
-
- Use conventional commit prefixes: `fix:`, `feat:`, `refactor:`, `investigate:`, `docs:`
|
|
439
|
-
- Be specific — "fix: billing page crash" is bad, "fix: payment_processor.charge() raises ZeroDivisionError when amount == 0" is good
|
|
440
|
-
- Include the component/domain if it helps: `fix(worker): queue leak in task_processor heartbeat loop`
|
|
441
|
-
- Never use vague words: "improve", "update", "change", "handle" without specifics
|
|
442
|
-
|
|
443
|
-
### 3B: Compose the body
|
|
444
|
-
|
|
445
|
-
Use this exact template. Every section is MANDATORY for bugs and features. Investigation issues use a simplified template (see 3C).
|
|
90
|
+
**Body** (pipeline template; `## Problem`, `## Affected Files` and `## Acceptance Criteria` are mandatory, the rest optional; in greenfield mode Affected Files may list *planned* paths inside the spike's declared repo layout):
|
|
446
91
|
|
|
447
92
|
```markdown
|
|
448
93
|
## Problem
|
|
449
|
-
|
|
450
|
-
{1-3 sentences describing what's wrong or what's missing. Be specific.}
|
|
451
|
-
|
|
452
|
-
{If bug: Include the error message, stack trace snippet, or observable symptom.}
|
|
453
|
-
{If regression: "Regression of #{N} — {brief description of original fix}."}
|
|
94
|
+
{1-3 specific sentences; error message or symptom; "Regression of #N" if applicable}
|
|
454
95
|
|
|
455
96
|
## Root Cause (if known)
|
|
456
|
-
|
|
457
|
-
{
|
|
458
|
-
{If the cause spans multiple files, list the chain:}
|
|
459
|
-
1. `{file1}:{line}` — {what happens here}
|
|
460
|
-
2. `{file2}:{line}` — {what happens here}
|
|
461
|
-
3. Result: {the failure}
|
|
462
|
-
|
|
463
|
-
{If root cause is unknown, say: "Root cause unknown — investigation needed." and list hypotheses.}
|
|
97
|
+
1. `{file}:{line}` — {what happens here}
|
|
98
|
+
{or "Root cause unknown — investigation needed." plus hypotheses}
|
|
464
99
|
|
|
465
100
|
## Affected Files
|
|
466
|
-
|
|
467
|
-
Files that need changes (ordered by dependency — change these in this order):
|
|
468
|
-
|
|
469
|
-
1. `{filepath}` — {what needs to change}
|
|
470
|
-
2. `{filepath}` — {what needs to change}
|
|
471
|
-
3. `{filepath}` — {what needs to change}
|
|
472
|
-
|
|
473
|
-
{CRITICAL: Include ALL files. If docker-compose.yml needs a change, docker-compose.prod.yml almost certainly does too. If a model changes, the migration file and any routers importing it need updating.}
|
|
101
|
+
1. `{filepath}` — {what needs to change} (dependency order; include override/companion files)
|
|
474
102
|
|
|
475
103
|
## Expected Behavior
|
|
476
|
-
|
|
477
|
-
{What should happen after the fix/feature is implemented.}
|
|
478
|
-
|
|
479
|
-
## Acceptance Criteria
|
|
480
|
-
|
|
481
|
-
- [ ] {Specific, testable criterion} [type:api]
|
|
482
|
-
- [ ] {Specific, testable criterion}
|
|
483
|
-
- [ ] {No regressions in {related_feature}}
|
|
484
|
-
|
|
485
|
-
> **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each criterion. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback.
|
|
486
|
-
|
|
487
|
-
## Dependencies
|
|
488
|
-
|
|
489
|
-
{If this issue depends on other issues:}
|
|
490
|
-
Depends on #{N} — {brief reason}
|
|
491
|
-
|
|
492
|
-
{If other issues should be done first:}
|
|
493
|
-
Should be done after #{N} — {brief reason}
|
|
494
|
-
|
|
495
|
-
{If no dependencies: omit this section entirely.}
|
|
496
|
-
|
|
497
|
-
## Additional Context
|
|
498
|
-
|
|
499
|
-
{Optional: screenshots, logs, links to Sentry/Grafana, user reports}
|
|
500
|
-
{Optional: "**Code branch**: `{branch}`" if the code only exists on a specific branch}
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
### 3C: Investigation issue template
|
|
504
|
-
|
|
505
|
-
For investigation/audit issues, use this simplified template:
|
|
506
|
-
|
|
507
|
-
```markdown
|
|
508
|
-
## Scope
|
|
509
|
-
|
|
510
|
-
{What needs to be investigated and why.}
|
|
511
|
-
|
|
512
|
-
## Questions to Answer
|
|
513
|
-
|
|
514
|
-
- [ ] {Specific question 1}
|
|
515
|
-
- [ ] {Specific question 2}
|
|
516
|
-
- [ ] {Specific question 3}
|
|
517
|
-
|
|
518
|
-
## Starting Points
|
|
519
|
-
|
|
520
|
-
Files/areas to examine:
|
|
521
|
-
1. `{filepath}` — {why start here}
|
|
522
|
-
2. `{filepath}` — {why start here}
|
|
523
|
-
|
|
524
|
-
## Deliverable
|
|
525
|
-
|
|
526
|
-
{What the investigation should produce:}
|
|
527
|
-
- Execution plan with specific sub-issues, OR
|
|
528
|
-
- Root cause analysis with fix recommendation, OR
|
|
529
|
-
- Decision document with trade-offs
|
|
530
|
-
|
|
531
|
-
## Context
|
|
532
|
-
|
|
533
|
-
{Background information the investigator needs.}
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
### 3D: Pipeline Issue Template (machine-callable reference)
|
|
537
|
-
|
|
538
|
-
> **Canonical Standard**: This template is the single source of truth for all automated issue creation in the ForgeDock pipeline. `milestone.md`, `orchestrate.md`, and `work-on/decompose.md` MUST use this template structure when creating issues programmatically. A shared convention is only useful if all create-paths enforce it — divergent inline templates in each command file cause acceptance-criteria drift and coverage gaps. See `work-on/decompose.md` Phase D0 for the investigation-gate pattern that enforces this standard at decomposition time. <!-- Added: forge#293 -->
|
|
539
|
-
|
|
540
|
-
When pipeline agents create issues programmatically (not via user input), use this canonical template. All automated `gh issue create` calls across Forge commands MUST include these mandatory sections. Domain-specific sections are additive — preserve them, but wrap them with this structure.
|
|
541
|
-
|
|
542
|
-
```markdown
|
|
543
|
-
## Problem
|
|
544
|
-
|
|
545
|
-
{1-3 sentences: what's wrong or what's missing. Specific and concrete.}
|
|
546
|
-
|
|
547
|
-
## Root Cause (if known)
|
|
548
|
-
|
|
549
|
-
{Point to the specific code path. Use `file:line` references. If unknown, say "Root cause unknown — investigation needed."}
|
|
550
|
-
|
|
551
|
-
## Affected Files
|
|
552
|
-
|
|
553
|
-
Files that need changes (ordered by dependency):
|
|
554
|
-
1. `{filepath}` — {what needs to change}
|
|
555
|
-
2. `{filepath}` — {what needs to change}
|
|
104
|
+
{what happens after the change}
|
|
556
105
|
|
|
557
106
|
## Acceptance Criteria
|
|
558
|
-
|
|
559
|
-
- [ ] {Specific, testable criterion} [type:api]
|
|
560
|
-
- [ ] {Specific, testable criterion}
|
|
561
|
-
|
|
562
|
-
> **Test-type annotation** (optional): Append `[type:api]`, `[type:unit]`, `[type:e2e]`, or `[type:manual]` to each criterion. The test gate reads this annotation directly and skips regex inference. Omit it to rely on regex classification fallback.
|
|
563
|
-
|
|
564
|
-
## Context
|
|
565
|
-
|
|
566
|
-
{Domain-specific context — logs, metrics, source report, linked issue, etc.}
|
|
107
|
+
- [ ] {specific, testable criterion} [type:api|unit|e2e|manual]
|
|
567
108
|
|
|
568
109
|
## Dependencies
|
|
569
|
-
|
|
570
|
-
{If this depends on other issues: "Depends on #{N} — {reason}". If none, omit this section.}
|
|
110
|
+
Depends on #{N} — {reason} (omit when none)
|
|
571
111
|
|
|
572
112
|
## Prior Investigation
|
|
573
|
-
|
|
574
|
-
{Optional — include when this issue was spawned from or relates to a prior investigation that produced Knowledge Gists.}
|
|
575
|
-
|
|
576
|
-
{For each Gist URL from the parent/sibling investigation:}
|
|
577
|
-
<!-- FORGE:PRIOR_GIST: {gist_url} -->
|
|
578
|
-
- {gist_url}
|
|
579
|
-
|
|
580
|
-
{If no prior investigation Gists exist, omit this section entirely.}
|
|
113
|
+
<!-- FORGE:PRIOR_GIST: {gist_url} --> (only when parent investigation Gists exist)
|
|
581
114
|
```
|
|
582
115
|
|
|
583
|
-
|
|
584
|
-
- Title MUST use conventional commit prefix: `fix:`, `feat:`, `refactor:`, `investigate:`, `docs:`
|
|
585
|
-
- `## Problem` is MANDATORY — every issue must state what's wrong or what's needed
|
|
586
|
-
- `## Affected Files` is MANDATORY — list actual file paths the investigator should read first
|
|
587
|
-
- `## Acceptance Criteria` is MANDATORY — at least one testable `- [ ]` criterion; each item MAY carry an optional `[type:api|unit|e2e|manual]` annotation for deterministic test-gate classification (omit to fall back to regex inference)
|
|
588
|
-
- Domain-specific sections (Evidence Trail, Pattern Metadata, Validation Checklist, etc.) SHOULD be preserved — they add pipeline value. Add mandatory sections around them, not instead of them.
|
|
589
|
-
- `## Prior Investigation` is OPTIONAL — include only when parent/sibling investigation Gist URLs are available. Each Gist URL must be wrapped in a `<!-- FORGE:PRIOR_GIST: {url} -->` annotation for machine-readable parsing by downstream agents. <!-- Added: forge#339 -->
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
### 3E: Validate the draft
|
|
594
|
-
|
|
595
|
-
Before creating, verify:
|
|
596
|
-
|
|
597
|
-
1. **Title follows convention**: `{prefix}({scope}): {description}` or `{prefix}: {description}`
|
|
598
|
-
2. **Affected files are real**: Every file path in the body exists in the repo (check with `git show origin/staging:{path}` or `git show origin/main:{path}`)
|
|
599
|
-
3. **No vague sections**: Every section has concrete content, not placeholders
|
|
600
|
-
4. **Acceptance criteria are testable**: Each criterion can be verified with a specific action
|
|
601
|
-
5. **Override files included**: If `docker-compose.yml` is listed, `docker-compose.prod.yml` is too (if it exists). If a model is listed, its migration is too.
|
|
602
|
-
6. **Priority matches severity**: P0 = prod down, P1 = significant user impact, P2 = minor, P3 = cosmetic
|
|
603
|
-
|
|
604
|
-
**Skip this step entirely in programmatic mode** — the caller already composed and is responsible for the draft. Programmatic mode has its own validation gate instead: Phase 3F below.
|
|
605
|
-
|
|
606
|
-
### 3F: Programmatic Pre-Create Validation (programmatic mode only, MANDATORY)
|
|
116
|
+
Investigation issues use `## Scope`, `## Questions to Answer` (checkboxes), `## Starting Points`, `## Deliverable`, `## Context`. Keep domain-specific sections (Evidence Trail, Validation Checklist, ...) and wrap the mandatory ones around them; `milestone.md`, `orchestrate.md` and `work-on/decompose.md` use this same template.
|
|
607
117
|
|
|
608
|
-
**
|
|
609
|
-
|
|
610
|
-
Runs after Phase 2D (dedup) passes and before Phase 4B (create). This reuses the exact same three mandatory-section check Phase 4C.5 performs post-creation — but applied here, pre-create, against the caller-supplied body, so a programmatic caller's issue is validated *before* it exists rather than repaired after the fact. Phase 4C.5 still runs afterward too (unchanged, for both modes) as an idempotent safety net — after this step it will find nothing to repair.
|
|
118
|
+
**Validate**: conventional prefix; no placeholder sections; each criterion is testable; override files included; priority matches severity. **Affected files are real**: every backticked path in `## Affected Files` exists at `origin/staging` or `origin/main`; in greenfield mode a missing path passes as `PLANNED` when it is declared in, or sits under a directory declared in, the spike doc's `Repo layout` section. Write the draft to an entity-scoped `mktemp` body file, then run from the target repo (`{GREENFIELD}`/`{SPIKE_DOC}` are the parsed values). Exit 1 (`MISSING <path>`): fix the path, or in greenfield mode add it to the spike layout. Exit 2: the spike doc is missing or declares no layout; fix it or drop `--greenfield`. If the helper does not resolve, check each path with `git show origin/staging:{path}` / `git show origin/main:{path}`.
|
|
611
119
|
|
|
612
120
|
```bash
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
Root cause unknown — investigation needed."
|
|
626
|
-
|
|
627
|
-
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
628
|
-
## Affected Files
|
|
629
|
-
|
|
630
|
-
Files to be identified during investigation."
|
|
631
|
-
|
|
632
|
-
echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
|
|
633
|
-
## Acceptance Criteria
|
|
634
|
-
|
|
635
|
-
- [ ] Fix confirmed during investigation."
|
|
636
|
-
|
|
637
|
-
# Append missing sections to the supplied body (never replace — only extend),
|
|
638
|
-
# same non-blocking "repair, don't fail" semantics as Phase 4C.5.
|
|
639
|
-
PROGRAMMATIC_BODY="${PROGRAMMATIC_BODY}${APPEND_TEXT}"
|
|
640
|
-
echo "Body repaired in-memory before creation — added:$MISSING_SECTIONS"
|
|
121
|
+
cd "{REPO_PATH}" || exit 1
|
|
122
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
123
|
+
AF_CHECK_SCRIPT=$(forge_resolve affected-files-check.sh)
|
|
124
|
+
GREENFIELD="{GREENFIELD}"; SPIKE_DOC="{SPIKE_DOC}"
|
|
125
|
+
# Declared spike layout auto-enables greenfield mode; without one, brownfield (existence only) applies.
|
|
126
|
+
[ -f "$SPIKE_DOC" ] && grep -qiE '^#+[[:space:]].*repo layout' "$SPIKE_DOC" && GREENFIELD=true
|
|
127
|
+
SPIKE_ARGS=()
|
|
128
|
+
[ "$GREENFIELD" = "true" ] && SPIKE_ARGS=(--spike "$SPIKE_DOC")
|
|
129
|
+
git fetch -q origin {STAGING_BRANCH} {DEFAULT_BRANCH} 2>/dev/null || true
|
|
130
|
+
if [ -n "$AF_CHECK_SCRIPT" ]; then
|
|
131
|
+
bash "$AF_CHECK_SCRIPT" "{DRAFT_BODY_FILE}" --ref "origin/{STAGING_BRANCH}" --ref "origin/{DEFAULT_BRANCH}" "${SPIKE_ARGS[@]}"
|
|
132
|
+
echo "AF_CHECK_EXIT=$?"
|
|
641
133
|
else
|
|
642
|
-
echo "
|
|
134
|
+
echo "affected-files-check.sh unresolved: check each path with git show (brownfield rule)"
|
|
643
135
|
fi
|
|
644
136
|
```
|
|
645
137
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
---
|
|
649
|
-
|
|
650
|
-
## Phase 4: Create the Issue
|
|
138
|
+
### 4. Create
|
|
651
139
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
Once Phase 2D (dedup) has passed with no blocking duplicate, and either Phase 3E (interactive draft validation) or Phase 3F (programmatic pre-create validation) has passed, proceed directly to Phase 4B and create the issue. **No pre-create confirmation prompt.** This is the default for both interactive and programmatic invocations — dedup and validation are the correctness gates, not a human approval step.
|
|
655
|
-
|
|
656
|
-
In programmatic mode, `{title}` = `$PROGRAMMATIC_TITLE`, `{full issue body}` = `$PROGRAMMATIC_BODY` (post-3F, i.e. already section-repaired), `{priority}, {category}` = the joined `PROGRAMMATIC_LABELS[@]`, and `{milestone}` = `$PROGRAMMATIC_MILESTONE` (or "none" if empty) — no interactive drafting occurred, so the dry-run preview below simply echoes back what the caller supplied (plus any 3F repairs).
|
|
657
|
-
|
|
658
|
-
**If `--dry-run` was passed** (see Argument Parsing): print the draft below and STOP. Do NOT run Phase 4B.
|
|
659
|
-
|
|
660
|
-
```
|
|
661
|
-
## Issue Draft (--dry-run — not created)
|
|
662
|
-
|
|
663
|
-
**Repo**: {GH_REPO}
|
|
664
|
-
**Title**: {title}
|
|
665
|
-
**Labels**: {priority}, {category}
|
|
666
|
-
**Milestone**: {milestone or "none"}
|
|
667
|
-
|
|
668
|
-
---
|
|
669
|
-
|
|
670
|
-
{full issue body}
|
|
671
|
-
|
|
672
|
-
---
|
|
673
|
-
|
|
674
|
-
Re-run without --dry-run to create this issue.
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
### 4B: Create the issue
|
|
678
|
-
|
|
679
|
-
**Never use `gh issue create` for this step.** GitHub CLI can report success with
|
|
680
|
-
empty output when GitHub rejects content creation with a secondary-rate-limit 403.
|
|
681
|
-
Use `gh api --method POST`, which preserves that API failure, and require a unique
|
|
682
|
-
token to survive a read-back before treating the issue as created. The token is
|
|
683
|
-
generated by this caller, not inferred from a title search, so a similarly titled
|
|
684
|
-
existing issue cannot be mistaken for the new one.
|
|
140
|
+
Write the draft to a body file per the integrity contract above (role `issue`), then make one call with the same exit-code contract:
|
|
685
141
|
|
|
686
142
|
```bash
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
CREATE_RESPONSE=$(gh api "repos/${GH_REPO}/issues" --method POST \
|
|
692
|
-
-f title="{TITLE}" -f body="$CREATE_BODY" \
|
|
693
|
-
-f 'labels[]={PRIORITY_LABEL}' -f 'labels[]={CATEGORY_LABEL}') || {
|
|
694
|
-
echo "ERROR: GitHub rejected issue creation; no issue was created." >&2
|
|
695
|
-
exit 1
|
|
696
|
-
}
|
|
697
|
-
NEW_NUMBER=$(echo "$CREATE_RESPONSE" | jq -r '.number // empty')
|
|
698
|
-
[ -n "$NEW_NUMBER" ] || { echo "ERROR: issue creation returned no issue number." >&2; exit 1; }
|
|
699
|
-
CREATED_BODY=$(gh issue view "$NEW_NUMBER" {GH_FLAG} --json body --jq '.body') || exit 1
|
|
700
|
-
printf '%s' "$CREATED_BODY" | grep -qF "issue-create-token:${CREATE_TOKEN}" || {
|
|
701
|
-
echo "ERROR: issue #${NEW_NUMBER} did not contain this create token; refusing to report success." >&2
|
|
702
|
-
exit 1
|
|
703
|
-
}
|
|
704
|
-
if [ -n "${MILESTONE_TITLE:-}" ]; then
|
|
705
|
-
[ "${DRY_RUN:-false}" != "true" ] || { echo "ERROR: refusing milestone edit during dry run." >&2; exit 1; }
|
|
706
|
-
gh issue edit "$NEW_NUMBER" {GH_FLAG} --milestone "$MILESTONE_TITLE" || exit 1
|
|
707
|
-
fi
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
**Programmatic mode variable mapping**: `{TITLE}` = `$PROGRAMMATIC_TITLE`; `{FULL_BODY}` = `$PROGRAMMATIC_BODY` (post-Phase-3F); the milestone branch is used iff `$PROGRAMMATIC_MILESTONE` is non-empty, with `{MILESTONE_TITLE}` = `$PROGRAMMATIC_MILESTONE`. This is the same verified REST-create path the interactive mode uses — no new command surface, only a new source for the variables.
|
|
711
|
-
|
|
712
|
-
`{PRIORITY_LABEL},{CATEGORY_LABEL}` in the interactive-mode template above stands for `PROGRAMMATIC_LABELS[@]`. REST API labels are passed as repeated `labels[]` fields, so labels remain distinct and zero labels need no field:
|
|
713
|
-
|
|
714
|
-
```bash
|
|
715
|
-
CREATE_TOKEN="forge-create-$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM"
|
|
716
|
-
CREATE_BODY="${PROGRAMMATIC_BODY}
|
|
717
|
-
|
|
718
|
-
<!-- issue-create-token:${CREATE_TOKEN} -->"
|
|
719
|
-
API_LABELS=()
|
|
720
|
-
for label in "${PROGRAMMATIC_LABELS[@]}"; do
|
|
721
|
-
API_LABELS+=(-f "labels[]=$label")
|
|
722
|
-
done
|
|
723
|
-
CREATE_RESPONSE=$(gh api "repos/${GH_REPO}/issues" --method POST \
|
|
724
|
-
-f title="$PROGRAMMATIC_TITLE" -f body="$CREATE_BODY" "${API_LABELS[@]}") || exit 1
|
|
725
|
-
NEW_NUMBER=$(echo "$CREATE_RESPONSE" | jq -r '.number // empty')
|
|
726
|
-
[ -n "$NEW_NUMBER" ] || { echo "ERROR: issue creation returned no issue number." >&2; exit 1; }
|
|
727
|
-
CREATED_BODY=$(gh issue view "$NEW_NUMBER" {GH_FLAG} --json body --jq '.body') || exit 1
|
|
728
|
-
printf '%s' "$CREATED_BODY" | grep -qF "issue-create-token:${CREATE_TOKEN}" || {
|
|
729
|
-
echo "ERROR: issue #${NEW_NUMBER} did not contain this create token; refusing to report success." >&2
|
|
730
|
-
exit 1
|
|
731
|
-
}
|
|
732
|
-
if [ -n "$PROGRAMMATIC_MILESTONE" ]; then
|
|
733
|
-
[ "${DRY_RUN:-false}" != "true" ] || { echo "ERROR: refusing milestone edit during dry run." >&2; exit 1; }
|
|
734
|
-
gh issue edit "$NEW_NUMBER" {GH_FLAG} --milestone "$PROGRAMMATIC_MILESTONE" || exit 1
|
|
735
|
-
fi
|
|
143
|
+
type forge_resolve >/dev/null 2>&1 || { setopt nonomatch 2>/dev/null || true; FORGE_SOURCED_ROOT='${CLAUDE_PLUGIN_ROOT}'; for _r in "$(cygpath -u "${FORGEDOCK_HOME:-}" 2>/dev/null || printf %s "${FORGEDOCK_HOME:-}")" '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGE_HOME:-}" "$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null | sed 's|/commands/work-on\.md$||')" "$(head -n 1 "${CODEX_HOME:-$HOME/.codex}/forge-home" 2>/dev/null)" "$HOME/.claude/plugins/marketplaces/forgedock" "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"/plugins/cache/forgedock/forgedock/* "$HOME/.claude"/plugins/cache/forgedock/forgedock/*; do case "$_r" in /*) [ -f "$_r/scripts/lib/forge-resolve.sh" ] && { . "$_r/scripts/lib/forge-resolve.sh"; break; };; esac; done; type forge_resolve >/dev/null 2>&1 || forge_resolve() { :; }; }
|
|
144
|
+
ISSUE_CREATE_SCRIPT=$(forge_resolve issue-create.sh)
|
|
145
|
+
[ -n "$ISSUE_CREATE_SCRIPT" ] || { echo "ERROR: issue-create.sh not found in a trusted ForgeDock install" >&2; exit 3; }
|
|
146
|
+
bash "$ISSUE_CREATE_SCRIPT" --title "{TITLE}" --body-file "$BODY_FILE" --label "{PRIORITY_LABEL}" --label "{CATEGORY_LABEL}" {--milestone "M"} {--repo OWNER/NAME} {--dry-run}
|
|
736
147
|
```
|
|
737
148
|
|
|
738
|
-
|
|
149
|
+
Report `**#{N}**: {title}`, URL, labels, milestone, and "Run `/work-on {N}` to start the pipeline."
|
|
739
150
|
|
|
740
|
-
|
|
741
|
-
# The mandatory create-token read-back in 4B is the success gate. This read
|
|
742
|
-
# obtains report metadata only; it must not replace that gate with a title search.
|
|
743
|
-
ISSUE_URL=$(gh issue view "$NEW_NUMBER" {GH_FLAG} --json url --jq '.url')
|
|
744
|
-
LABELS=$(gh issue view {NEW_NUMBER} {GH_FLAG} --json labels --jq '[.labels[].name] | join(", ")')
|
|
745
|
-
echo "Created: ${ISSUE_URL}"
|
|
746
|
-
echo "Labels: ${LABELS}"
|
|
747
|
-
echo "ISSUE_CREATE_RESULT:CREATED number=${NEW_NUMBER} url=${ISSUE_URL}"
|
|
748
|
-
```
|
|
749
|
-
|
|
750
|
-
**`--body-file` integrity read-back (MANDATORY)**: Immediately after every programmatic creation sourced from `--body-file`, re-read the issue body and assert the caller's exact marker is present. This is a hard error, not a warning: a missing marker means the write may have received another agent's plausible-looking body.
|
|
751
|
-
|
|
752
|
-
```bash
|
|
753
|
-
if [ -n "$PROGRAMMATIC_BODY_FILE" ]; then
|
|
754
|
-
CREATED_BODY=$(gh issue view {NEW_NUMBER} {GH_FLAG} --json body --jq '.body') || exit 1
|
|
755
|
-
if ! printf '%s' "$CREATED_BODY" | grep -Fqx "$PROGRAMMATIC_BODY_MARKER"; then
|
|
756
|
-
echo "ERROR: created issue body is missing expected integrity marker $PROGRAMMATIC_BODY_MARKER" >&2
|
|
757
|
-
exit 1
|
|
758
|
-
fi
|
|
759
|
-
fi
|
|
760
|
-
```
|
|
151
|
+
**Batch**: draft every issue, create each in order (no confirmation prompt), and report a table `| # | Issue | Priority | Category | Deps |`. With `--dry-run`, print the draft table plus each body and create nothing.
|
|
761
152
|
|
|
762
|
-
|
|
153
|
+
## Anti-Patterns
|
|
763
154
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
After creation, verify the issue body contains the three mandatory pipeline sections. If any are missing, add placeholder content so downstream pipeline agents have the correct scaffolding.
|
|
773
|
-
|
|
774
|
-
**Runs for both interactive and programmatic invocations, unconditionally — do not skip this step for programmatic mode.** For programmatic callers this is expected to be a no-op: Phase 3F already validated and repaired `$PROGRAMMATIC_BODY` before creation, so `$CREATED_BODY` here should already contain all three sections. This step still runs as an idempotent safety net (e.g. if GitHub or an intermediate transform altered the body between 3F and creation).
|
|
775
|
-
|
|
776
|
-
**This step never fails or blocks.** Missing sections cause a repair, not an error. Issues created with full structure pass through immediately.
|
|
777
|
-
|
|
778
|
-
```bash
|
|
779
|
-
CREATED_BODY=$(gh issue view {NEW_NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
780
|
-
|
|
781
|
-
MISSING_SECTIONS=""
|
|
782
|
-
echo "$CREATED_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
|
|
783
|
-
echo "$CREATED_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
|
|
784
|
-
echo "$CREATED_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
|
|
785
|
-
|
|
786
|
-
if [ -n "$MISSING_SECTIONS" ]; then
|
|
787
|
-
echo "WARNING: Issue body is missing sections:$MISSING_SECTIONS — adding placeholders to preserve pipeline compatibility"
|
|
788
|
-
|
|
789
|
-
APPEND_TEXT=""
|
|
790
|
-
echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
|
|
791
|
-
## Problem
|
|
792
|
-
|
|
793
|
-
Root cause unknown — investigation needed."
|
|
794
|
-
|
|
795
|
-
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
796
|
-
## Affected Files
|
|
797
|
-
|
|
798
|
-
Files to be identified during investigation."
|
|
799
|
-
|
|
800
|
-
echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
|
|
801
|
-
## Acceptance Criteria
|
|
802
|
-
|
|
803
|
-
- [ ] Fix confirmed during investigation."
|
|
804
|
-
|
|
805
|
-
# Append missing sections to the existing body (never replace — only extend)
|
|
806
|
-
REPAIRED_BODY="${CREATED_BODY}${APPEND_TEXT}"
|
|
807
|
-
gh issue edit {NEW_NUMBER} {GH_FLAG} --body "$REPAIRED_BODY"
|
|
808
|
-
echo "Body repaired — added:$MISSING_SECTIONS"
|
|
809
|
-
else
|
|
810
|
-
echo "Body validation passed — all mandatory sections present"
|
|
811
|
-
fi
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
### 4D: Add to Project board (if configured)
|
|
815
|
-
|
|
816
|
-
```bash
|
|
817
|
-
# Add to project board if forge.yaml → project_board is configured
|
|
818
|
-
PROJECT_NUMBER=$(yq '.project_board.project_number // ""' "$CONFIG_FILE")
|
|
819
|
-
PROJECT_BOARD_OWNER=$(yq '.project_board.owner // .project.owner' "$CONFIG_FILE")
|
|
820
|
-
if [ -n "$PROJECT_NUMBER" ] && [ "$PROJECT_NUMBER" != "null" ]; then
|
|
821
|
-
gh project item-add "$PROJECT_NUMBER" --owner "$PROJECT_BOARD_OWNER" --url "${ISSUE_URL}" 2>/dev/null || true
|
|
822
|
-
fi
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
### 4E: Report to user
|
|
826
|
-
|
|
827
|
-
```
|
|
828
|
-
## Issue Created
|
|
829
|
-
|
|
830
|
-
**#{NUMBER}**: {title}
|
|
831
|
-
**URL**: {url}
|
|
832
|
-
**Labels**: {labels}
|
|
833
|
-
**Milestone**: {milestone or "none"}
|
|
834
|
-
|
|
835
|
-
{If ready to work on it: "Run `/work-on {NUMBER}` to start the pipeline."}
|
|
836
|
-
{If part of a milestone: "This issue will be picked up in the next `/orchestrate milestone {slug}` run."}
|
|
837
|
-
```
|
|
838
|
-
|
|
839
|
-
---
|
|
840
|
-
|
|
841
|
-
## Batch Mode
|
|
842
|
-
|
|
843
|
-
If the user provides multiple issues to create (e.g., from an audit, analytics session, or decomposition):
|
|
844
|
-
|
|
845
|
-
1. Draft ALL issues first (each running its own dedup + validation pass, Phase 2D/3E)
|
|
846
|
-
2. Create all of them by default — consistent with the single-issue default in Phase 4A. No pre-create confirmation prompt.
|
|
847
|
-
3. If `--dry-run` was passed, present them as a numbered list instead of creating them (see table below) and STOP
|
|
848
|
-
4. Report all created (or, under `--dry-run`, all drafted) issue numbers
|
|
849
|
-
|
|
850
|
-
**Default (no `--dry-run`)**: create each drafted issue in order, then report:
|
|
851
|
-
```
|
|
852
|
-
## Batch Issues Created
|
|
853
|
-
|
|
854
|
-
| # | Issue | Priority | Category | Deps |
|
|
855
|
-
|---|-------|----------|----------|------|
|
|
856
|
-
| 1 | #{N1} {title} | P2 | bug | — |
|
|
857
|
-
| 2 | #{N2} {title} | P1 | feature | After #{N1} |
|
|
858
|
-
| 3 | #{N3} {title} | P2 | refactor | — |
|
|
859
|
-
```
|
|
860
|
-
|
|
861
|
-
**With `--dry-run`**: print the batch draft table and stop before any `gh issue create` call:
|
|
862
|
-
```
|
|
863
|
-
## Batch Issue Draft (--dry-run — not created)
|
|
864
|
-
|
|
865
|
-
| # | Title | Priority | Category | Deps |
|
|
866
|
-
|---|-------|----------|----------|------|
|
|
867
|
-
| 1 | {title} | P2 | bug | — |
|
|
868
|
-
| 2 | {title} | P1 | feature | After #1 |
|
|
869
|
-
| 3 | {title} | P2 | refactor | — |
|
|
870
|
-
|
|
871
|
-
<details><summary>Issue 1: {title}</summary>
|
|
872
|
-
|
|
873
|
-
{full body}
|
|
874
|
-
|
|
875
|
-
</details>
|
|
876
|
-
|
|
877
|
-
<details><summary>Issue 2: {title}</summary>
|
|
878
|
-
|
|
879
|
-
{full body}
|
|
880
|
-
|
|
881
|
-
</details>
|
|
882
|
-
|
|
883
|
-
Re-run without --dry-run to create these {N} issues.
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
---
|
|
887
|
-
|
|
888
|
-
## Anti-Patterns — What NOT to Do
|
|
889
|
-
|
|
890
|
-
| Bad Issue | Why It Fails | Good Version |
|
|
891
|
-
|-----------|-------------|-------------|
|
|
892
|
-
| "Fix docker mount" | Which compose file? Which mount? | "fix: `logs` volume in docker-compose.yml missing `rw` flag — container writes fail at runtime" |
|
|
893
|
-
| "Update billing" | What about billing? What's broken? | "fix: payment_processor.charge() raises ZeroDivisionError when amount == 0" |
|
|
894
|
-
| "Investigate performance" | Investigate what? Where? | "investigate: API p95 latency doubled since last deploy — trace hot paths in job processing routers" |
|
|
895
|
-
| No affected files listed | Investigator guesses wrong files | List every file with what needs to change |
|
|
896
|
-
| "Handle edge case" | Which edge case? In what function? | "fix: task_processor.process_job() silently drops jobs when queue.length > MAX_BATCH" |
|
|
897
|
-
| Missing override files | Fix is incomplete, deploy fails | Include docker-compose.prod.yml, .env.example, etc. |
|
|
898
|
-
| P0 for a typo fix | Wastes priority signaling | Use P3 for cosmetic issues |
|
|
899
|
-
|
|
900
|
-
---
|
|
155
|
+
| Bad | Why it fails | Good |
|
|
156
|
+
|-----|-------------|------|
|
|
157
|
+
| "Fix docker mount" | Which file? Which mount? | "fix: `logs` volume in docker-compose.yml missing `rw` — container writes fail" |
|
|
158
|
+
| "Update billing" | What is broken? | "fix: payment_processor.charge() raises ZeroDivisionError when amount == 0" |
|
|
159
|
+
| "Investigate performance" | Where? | "investigate: API p95 doubled since last deploy — trace job routers" |
|
|
160
|
+
| No affected files | Investigator guesses wrong | List every file with its change |
|
|
161
|
+
| Missing override files | Incomplete fix, deploy fails | Include docker-compose.prod.yml, .env.example |
|
|
162
|
+
| P0 for a typo | Dilutes priority | P3 for cosmetic |
|
|
901
163
|
|
|
902
164
|
## Safety Rules
|
|
903
165
|
|
|
904
|
-
1.
|
|
905
|
-
2.
|
|
906
|
-
3.
|
|
907
|
-
4.
|
|
908
|
-
5.
|
|
909
|
-
6. **Use conventional commit prefixes in titles.** The pipeline and changelog depend on them.
|
|
910
|
-
7. **Default to P2.** Only use P0/P1 when the user explicitly indicates severity or the evidence clearly shows production impact.
|
|
911
|
-
8. **`--title` is the sole programmatic-mode trigger.** `--body`/`--body-file`/`--label`/`--milestone` are meaningless without it and never activate programmatic mode on their own — this prevents an accidental partial flag set from silently skipping Phase 1/3.
|
|
166
|
+
1. Read code before an interactive issue; never fabricate file paths. (Programmatic callers own this.)
|
|
167
|
+
2. Dedup runs for every create, interactive or programmatic; no flag disables it. Agents never bypass a dedup STOP without user authorization.
|
|
168
|
+
3. Create by default; pause only for `--dry-run`.
|
|
169
|
+
4. Never create issues with `gh issue create` (it can report success on a swallowed 403): always go through the script, which creates through `gh api --method POST` and succeeds only after the create-token read-back. Apply the same pattern to `gh issue comment`, `gh pr comment` and `gh pr create`: POST with a caller-generated token, GET the returned object, require the token.
|
|
170
|
+
5. Conventional-commit prefixes in titles; default to P2.
|