forgedock 1.26.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.
Files changed (137) hide show
  1. package/README.md +4 -1
  2. package/bin/bootstrap.mjs +55 -0
  3. package/bin/engine/outcome-codes.mjs +13 -0
  4. package/bin/engine/phases.mjs +38 -2
  5. package/bin/engine/telemetry-seam.mjs +41 -6
  6. package/bin/engine/telemetry-seam.test.mjs +35 -0
  7. package/bin/engine-cli.mjs +616 -54
  8. package/bin/engine.mjs +13 -5
  9. package/bin/forgedock.mjs +169 -39
  10. package/bin/gc.mjs +580 -0
  11. package/bin/init-detect.mjs +68 -3
  12. package/bin/init-enrich.mjs +3 -0
  13. package/bin/init-repo.mjs +213 -0
  14. package/bin/journey.mjs +278 -43
  15. package/bin/lean/admission.mjs +33 -3
  16. package/bin/lean/batch.mjs +24 -5
  17. package/bin/lean/batch.test.mjs +54 -4
  18. package/bin/lean/checks.mjs +199 -0
  19. package/bin/lean/checks.test.mjs +183 -0
  20. package/bin/lean/conformance.mjs +235 -37
  21. package/bin/lean/conformance.test.mjs +143 -5
  22. package/bin/lean/credentials.mjs +29 -0
  23. package/bin/lean/depcache.mjs +312 -0
  24. package/bin/lean/depcache.test.mjs +298 -0
  25. package/bin/lean/drift.mjs +198 -0
  26. package/bin/lean/drift.test.mjs +223 -0
  27. package/bin/lean/effects.mjs +178 -16
  28. package/bin/lean/execution.e2e.test.mjs +81 -0
  29. package/bin/lean/execution.mjs +431 -0
  30. package/bin/lean/execution.test.mjs +583 -0
  31. package/bin/lean/heavy.mjs +289 -0
  32. package/bin/lean/heavy.test.mjs +207 -0
  33. package/bin/lean/lean.mjs +877 -151
  34. package/bin/lean/lean.test.mjs +1798 -28
  35. package/bin/lean/ledger.mjs +92 -0
  36. package/bin/lean/ledger.test.mjs +100 -0
  37. package/bin/lean/limits.mjs +58 -0
  38. package/bin/lean/limits.test.mjs +150 -0
  39. package/bin/lean/orchestrate.mjs +321 -19
  40. package/bin/lean/prompts.mjs +125 -17
  41. package/bin/lean/refactor.mjs +276 -0
  42. package/bin/lean/refactor.test.mjs +359 -0
  43. package/bin/lean/render.mjs +25 -6
  44. package/bin/lean/review-trigger.mjs +134 -0
  45. package/bin/lean/review-trigger.test.mjs +104 -0
  46. package/bin/lean/sandbox-provider.contract.test.mjs +257 -0
  47. package/bin/lean/sandbox-remote.mjs +142 -0
  48. package/bin/lean/sched.mjs +221 -0
  49. package/bin/lean/sched.test.mjs +310 -0
  50. package/bin/lean/schedd.mjs +247 -0
  51. package/bin/lean/schedd.test.mjs +180 -0
  52. package/bin/lean/toolchain.mjs +228 -0
  53. package/bin/lean/toolchain.test.mjs +154 -0
  54. package/bin/lean/verify.mjs +124 -0
  55. package/bin/lean/verify.test.mjs +118 -0
  56. package/bin/lean/worktree.mjs +69 -0
  57. package/bin/lean/worktree.test.mjs +141 -0
  58. package/bin/runner.mjs +53 -10
  59. package/bin/stats/github-source.mjs +1 -1
  60. package/bin/stats/index.mjs +14 -2
  61. package/bin/stats/query.mjs +72 -0
  62. package/bin/stats/stats.test.mjs +54 -0
  63. package/commands/adopt.md +3 -0
  64. package/commands/audit.md +5 -8
  65. package/commands/autopilot.md +2 -0
  66. package/commands/bootstrap.md +35 -0
  67. package/commands/cleanup.md +7 -22
  68. package/commands/forge-stats.md +2 -1
  69. package/commands/forgedock-init.md +125 -6
  70. package/commands/issue.md +89 -830
  71. package/commands/milestone.md +55 -6
  72. package/commands/orchestrate.md +10 -2
  73. package/commands/review-pr.md +2 -2
  74. package/commands/upgrade-deps.md +3 -3
  75. package/commands/work-on/build/validate.md +4 -0
  76. package/commands/work-on/close.md +2 -0
  77. package/commands/work-on/decompose.md +24 -6
  78. package/docs/CI.md +3 -3
  79. package/docs/CONFIG.md +227 -3
  80. package/docs/OUTCOME-CODES.md +11 -0
  81. package/docs/TELEMETRY.md +1 -1
  82. package/docs/site/command-reference.md +22 -0
  83. package/docs/site/getting-started.md +2 -0
  84. package/docs/site/start-a-new-project.md +327 -0
  85. package/package.json +1 -1
  86. package/scripts/affected-files-check.sh +91 -0
  87. package/scripts/affected-files-check.test.sh +80 -0
  88. package/scripts/bootstrap-create.mjs +393 -0
  89. package/scripts/bootstrap-validate.mjs +321 -0
  90. package/scripts/calibration.mjs +116 -0
  91. package/scripts/check-devdocs-freshness.sh +125 -0
  92. package/scripts/check-devdocs-freshness.test.sh +130 -0
  93. package/scripts/check-spec-markers.sh +3 -0
  94. package/scripts/check-spec-size.sh +29 -11
  95. package/scripts/check-spec-size.test.sh +27 -4
  96. package/scripts/ci-local/lib.mjs +8 -1
  97. package/scripts/ci-local/parallelism.test.mjs +27 -0
  98. package/scripts/ci-local/run.mjs +16 -5
  99. package/scripts/ci-local/scoping.mjs +82 -0
  100. package/scripts/ci-local.test.sh +14 -2
  101. package/scripts/fixtures/symbol-index/other/sample.rb +3 -0
  102. package/scripts/fixtures/symbol-index/py/pkg/__init__.py +1 -0
  103. package/scripts/fixtures/symbol-index/py/pkg/a.py +5 -0
  104. package/scripts/fixtures/symbol-index/py/pkg/b.py +14 -0
  105. package/scripts/fixtures/symbol-index/py/pkg/c.py +1 -0
  106. package/scripts/fixtures/symbol-index/py/sample.py +41 -0
  107. package/scripts/fixtures/symbol-index/ts/sample.ts +13 -0
  108. package/scripts/fixtures/symbol-move/gen_big.py +201 -0
  109. package/scripts/fixtures/symbol-move-ts/gen_big.mjs +56 -0
  110. package/scripts/forge-root.test.sh +1 -1
  111. package/scripts/hotspots.sh +100 -0
  112. package/scripts/hotspots.test.sh +101 -0
  113. package/scripts/issue-create.sh +197 -0
  114. package/scripts/issue-create.test.sh +166 -0
  115. package/scripts/maintenance-scan.sh +185 -0
  116. package/scripts/maintenance-scan.test.sh +89 -0
  117. package/scripts/milestone-drift.sh +206 -0
  118. package/scripts/milestone-drift.test.sh +120 -0
  119. package/scripts/northstar-docs.test.sh +68 -0
  120. package/scripts/refactor-verify.sh +368 -0
  121. package/scripts/refactor-verify.test.sh +202 -0
  122. package/scripts/review-prepare.sh +11 -9
  123. package/scripts/review-prepare.test.sh +14 -0
  124. package/scripts/structure-delta.sh +155 -0
  125. package/scripts/structure-delta.test.sh +136 -0
  126. package/scripts/symbol-index.mjs +253 -0
  127. package/scripts/symbol-index.py +354 -0
  128. package/scripts/symbol-index.test.sh +119 -0
  129. package/scripts/symbol-move-ts.test.sh +161 -0
  130. package/scripts/symbol-move.mjs +379 -0
  131. package/scripts/symbol-move.py +383 -0
  132. package/scripts/symbol-move.test.sh +168 -0
  133. package/templates/bootstrap/interview.md +34 -0
  134. package/templates/bootstrap/issue-body.md +42 -0
  135. package/templates/bootstrap/needs-human-issue.md +45 -0
  136. package/templates/bootstrap/stack-profiles.md +34 -0
  137. 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 exact structure the `/work-on` pipeline expects. Every issue you create must give the investigation agent enough context to find the right files on the first pass — no vague descriptions, no missing domains, no ambiguous scope.
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
- `/issue` is the structured create-hook for issue creation across the pipeline — it enforces the canonical template, reads code before drafting, runs dedup, and validates mandatory sections. Because those checks are deterministic, `/issue` **creates the issue by default once they pass** — it does not wait for a human to approve the draft. Pass `--dry-run` to review the draft without creating anything (see Argument Parsing below).
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) | Default — read code, dedup, draft, validate, then create the issue immediately. No pre-create confirmation prompt. |
30
- | `--dry-run` | Draft and validate as normal, but STOP before `gh issue create` — print the draft (or batch draft table) for human review instead of creating it |
31
- | `--title "..."` | **Activates programmatic mode.** Supplies the final issue title directly — skips Phase 1 (parse) and Phase 3 (draft) entirely. See Programmatic Invocation Contract below. |
32
- | `--body "..."` | Supplies the final issue body directly (programmatic mode only). Mutually exclusive with `--body-file`. |
33
- | `--body-file <path>` | Supplies the final issue body by reading it from a file (programmatic mode only). Mutually exclusive with `--body`. |
34
- | `--label "..."` | Adds one label. Repeatable (`--label bug --label P2`) — labels accumulate. Programmatic mode only. |
35
- | `--milestone "..."` | Supplies the milestone title directly, skipping the Phase 2E milestone lookup. Programmatic mode only, optional. |
36
- | `--exclude "N,N,..."` | Comma-separated issue numbers to exclude from the Phase 2D dedup candidate set. Programmatic mode only, optional. Forwarded verbatim to `scripts/issue-dedup.sh --exclude`. NOT a `--force` equivalent — narrows the candidate set, does not disable the gate. See Phase 2D. <!-- Added: forge#2432 --> |
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
- DRY_RUN=false
40
- PROGRAMMATIC_TITLE=""
41
- PROGRAMMATIC_BODY=""
42
- PROGRAMMATIC_BODY_FILE=""
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
- `--dry-run` never bypasses a correctness gate — the Phase 2D dedup STOP and the `--force` human-override rule (see Phase 2D) apply identically whether or not `--dry-run` is set, and identically in both interactive and programmatic mode.
179
-
180
- ### Programmatic Invocation Contract
44
+ The script prints exactly one result line; report it verbatim and exit with its code:
181
45
 
182
- For callers that have already composed a title, body, labels, and (optionally) a milestone — audit tools, `/work-on` decomposition, pipeline-health proposals, review-finding creation, and similar automation — pass `--title` to activate programmatic mode. This bypasses Phase 1 (free-text parsing) and Phase 3 (LLM drafting) entirely; Phase 2D (dedup) and body validation still run.
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
- **Required**: `--title "TEXT"` and exactly one of `--body "TEXT"` / `--body-file <path>`.
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
- If `--title` is present but neither `--body` nor `--body-file` is supplied, or both are supplied, this is a usage error: print `ERROR: programmatic mode requires exactly one of --body or --body-file` and STOP — do not fall through to Phase 2D or Phase 4B.
56
+ ### `--body-file` integrity contract (MANDATORY for the caller)
188
57
 
189
- **`--body-file` integrity contract (MANDATORY for the caller)**: Do not use a generic name under shared `/tmp`, even with `mktemp`. Prefer the session scratchpad (or a repo-relative scratch directory) because a Windows-native `gh` may not resolve Git Bash `/tmp` as expected. 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. Create the directory if needed, make the filename include the target issue or PR number and an agent-unique token, then let `mktemp` add its random suffix:
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 caller-chosen marker in the form `<!-- FORGE:BODY-INTEGRITY:<entity>_<role>_<agent-token> -->`. A unique path reduces collisions; it does not prove that the intended content reached GitHub. The caller must retain the marker until the write is verified. A collision can substitute plausible-looking content from another agent, so an eyeball check is not a sufficient backstop.
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
- ## Phase 1: Parse the Request
71
+ ## Interactive Mode (free text)
247
72
 
248
- **If `PROGRAMMATIC_MODE=true`** (see Programmatic Invocation Contract above): run **1A only** (repository resolution — `GH_REPO`/`GH_FLAG`/`REPO_PATH`/`STAGING_BRANCH` still come from `forge.yaml` and the multi-repo prefix table, never from a caller flag), then skip 1B/1C/1D (type/priority/category classification — the caller already supplied final `--label` values) and skip Phase 3 (drafting) entirely. Jump directly to Phase 2D using `{PROPOSED_TITLE}` = `$PROGRAMMATIC_TITLE`, then continue to Phase 3F (Programmatic Pre-Create Validation) and Phase 4 (create). Do not apply the free-text classification/drafting rules below — they apply only when `PROGRAMMATIC_MODE=false`.
73
+ ### 1. Parse the request
249
74
 
250
- The user's input (`$ARGUMENTS`) can be:
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
- | Input Form | Example |
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
- ### 1A: Resolve target repository
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
- ## Phase 2: Gather Context (MANDATORY)
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
- **You MUST read code before writing the issue.** This is the critical difference between a good issue and a bad one. The investigation agent inherits YOUR file references — if you point to the wrong file, the entire pipeline goes sideways.
86
+ ### 3. Draft
322
87
 
323
- **In programmatic mode, skip 2A/2B/2C (domain identification, code reading, affected-file enumeration)** — the caller already did its own domain-specific reading and drafting before invoking `/issue` with `--title`/`--body`/`--body-file`; re-deriving that here would duplicate work the caller has already done and risks second-guessing a caller with more direct context (e.g. an audit agent that already read the offending file). Continue to **2D** (dedup — still mandatory) and **2E** (skip — see below).
88
+ **Title**: `{prefix}({scope}): {specific description}`, max 80 characters, no vague verbs ("improve", "update", "handle") without specifics.
324
89
 
325
- ### 2A: Identify the domain
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
- {Point to the specific code path. Use `file:line` references.}
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
- **Rules for automated issue creation**:
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
- **Skip if `PROGRAMMATIC_MODE=false`** — this step only runs for the `--title`-driven path; the free-text/interactive path uses 3E above instead.
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
- MISSING_SECTIONS=""
614
- echo "$PROGRAMMATIC_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
615
- echo "$PROGRAMMATIC_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
616
- echo "$PROGRAMMATIC_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
617
-
618
- if [ -n "$MISSING_SECTIONS" ]; then
619
- echo "WARNING: supplied --body is missing sections:$MISSING_SECTIONS — adding placeholders before creation"
620
-
621
- APPEND_TEXT=""
622
- echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
623
- ## Problem
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 "Pre-create validation passed — all mandatory sections present"
134
+ echo "affected-files-check.sh unresolved: check each path with git show (brownfield rule)"
643
135
  fi
644
136
  ```
645
137
 
646
- **This step never fails or blocks** — identical to Phase 4C.5's contract. Missing sections cause an in-memory repair, not an error; `$PROGRAMMATIC_BODY` (now guaranteed to contain all three mandatory sections) is what Phase 4B creates the issue with, and what a `--dry-run` preview (Phase 4A) prints.
647
-
648
- ---
649
-
650
- ## Phase 4: Create the Issue
138
+ ### 4. Create
651
139
 
652
- ### 4A: Create by default, or stop for review with `--dry-run`
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
- CREATE_TOKEN="forge-create-$(date -u +%Y%m%dT%H%M%SZ)-$$-$RANDOM"
688
- CREATE_BODY="${FULL_BODY}
689
-
690
- <!-- issue-create-token:${CREATE_TOKEN} -->"
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
- ### 4C: Post-creation verification
149
+ Report `**#{N}**: {title}`, URL, labels, milestone, and "Run `/work-on {N}` to start the pipeline."
739
150
 
740
- ```bash
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
- ### Content-Creation Rule
153
+ ## Anti-Patterns
763
154
 
764
- Apply the same fail-closed pattern to `gh issue comment`, `gh pr comment`, and
765
- `gh pr create`: use `gh api --method POST`, include a caller-generated token in
766
- the submitted body, capture the returned object ID/number, then GET that exact
767
- object and assert the token is present. Empty output, a missing ID, or a failed
768
- read-back is a hard error, never a successful no-op or a search-indexing retry.
769
-
770
- ### 4C.5: Body Validation (MANDATORY)
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. **Always read code before creating an issue.** Never create an issue based solely on user description without verifying the files exist and the problem is plausible. (Programmatic mode: this responsibility shifts to the caller — the caller already read code before composing `--title`/`--body`; `/issue` does not re-verify file paths for programmatic invocations.)
905
- 2. **Always check for duplicates.** Creating duplicate issues wastes agent runs — dedup (Phase 2D) runs identically for interactive and programmatic invocations; no flag bypasses it.
906
- 3. **Default to non-interactive creation.** Once dedup (Phase 2D) and draft validation (Phase 3E, or Phase 3F for programmatic mode) pass, create the issue — do not wait for a human to approve the draft. Only pause for human review when `--dry-run` is explicitly passed.
907
- 4. **Never fabricate file paths.** Every path in the issue body must be verified against the actual repo.
908
- 5. **Never omit override/companion files.** If one compose file is affected, check the other. If one model is affected, check its migration.
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.