forgedock 1.8.1 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +11 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Investigate a GitHub issue — validate it's real, determine root cause, post findings
|
|
3
|
-
argument-hint: "
|
|
3
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
4
|
+
context: fork
|
|
4
5
|
---
|
|
5
6
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
7
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
@@ -11,7 +12,9 @@ argument-hint: "[issue number] [--repo {owner}/{repo}] [--gh-flag \"-R {owner}/{
|
|
|
11
12
|
|
|
12
13
|
Standalone investigation phase for the work-on pipeline. Validates whether an issue is real, determines root cause, posts a structured FORGE:INVESTIGATOR comment to GitHub, and updates workflow labels.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
This skill runs in an **isolated forked context**: it sees only this file and its args. It has no variables from the caller — every other piece of state is re-read from GitHub/git here. It ends by printing exactly one `INVESTIGATE_RESULT:` block (see `## Output`) as its final reply, on **every** exit path (success, ALREADY_DONE, INVALID, BLOCKED).
|
|
16
|
+
|
|
17
|
+
**Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154. This file's mechanical bits (1A label set, 1D label transitions) stay at this tier because they're interleaved with the reasoning-heavy investigation steps in the same forked `Skill()` invocation. <!-- Added: forge#1827 -->
|
|
15
18
|
**NEVER use plan mode (EnterPlanMode).**
|
|
16
19
|
|
|
17
20
|
<!-- FORGE:SPEC_LOADED — work-on/investigate.md loaded and active. Agent is bound by this spec. -->
|
|
@@ -20,15 +23,99 @@ Standalone investigation phase for the work-on pipeline. Validates whether an is
|
|
|
20
23
|
|
|
21
24
|
## Inputs
|
|
22
25
|
|
|
23
|
-
Parse from $ARGUMENTS:
|
|
26
|
+
Parse from $ARGUMENTS (`{NUMBER} --repo {GH_REPO} --gh-flag "{GH_FLAG}"`):
|
|
24
27
|
- `{NUMBER}` — issue number (required)
|
|
25
|
-
- `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`
|
|
28
|
+
- `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`)
|
|
26
29
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
The router passes all three. If `--repo` / `--gh-flag` are absent (standalone use), resolve them from `forge.yaml → project`.
|
|
32
|
+
|
|
33
|
+
**Fail closed**: if `{NUMBER}` is missing/non-numeric, or `{GH_REPO}` cannot be determined, print the result block below and stop — do not guess:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
INVESTIGATE_RESULT:
|
|
37
|
+
status: BLOCKED
|
|
38
|
+
verdict: null
|
|
39
|
+
confidence: null
|
|
40
|
+
decompose: null
|
|
41
|
+
comment_url: null
|
|
42
|
+
gist_url: null
|
|
43
|
+
milestone_index_url: null
|
|
44
|
+
blocker: missing required arg: <which one>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Execution order**: run the Phase 1A resume check FIRST. If it reports ALREADY_DONE, print that result and stop — skip Phases 0.5, 0.6 and everything else. Otherwise run 0.5 → 0.6 → 1A (label) → 1A.5 → 1B → 1C → 1C.5 → 1C.6 → 1D.
|
|
29
48
|
|
|
30
49
|
---
|
|
31
50
|
|
|
51
|
+
## Script resolution
|
|
52
|
+
|
|
53
|
+
**Shell state does not persist between Bash tool calls** (each call is a fresh shell): paste the block below at the top of every bash command in this skill that calls `resolve_script` or uses `FORGE_ROOT`/`UNIVERSAL_DIR`.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
57
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
58
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
59
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
60
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
61
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
62
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
63
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
64
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
65
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
66
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
67
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
68
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
69
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
70
|
+
ADAPTIVE_ENABLED=false
|
|
71
|
+
fi
|
|
72
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
73
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
74
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
75
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
76
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
77
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
78
|
+
FORGE_ROOT=""
|
|
79
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
80
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
81
|
+
_l="$HOME/.claude/commands/work-on.md"; _l="$(readlink -f "$_l" 2>/dev/null || readlink "$_l" 2>/dev/null)"; [ -n "$_l" ] && _l="$(dirname "$(dirname "$_l")")"
|
|
82
|
+
# newest cached version first: numeric major.minor.patch of the version dir name only; a release outranks its pre-release (1.10.0 > 1.9.0 > 1.9.0-rc1)
|
|
83
|
+
_v="$(find -L "$HOME/.claude/plugins/cache" -mindepth 3 -maxdepth 3 -type d 2>/dev/null | awk -F/ '$(NF-1)=="forgedock"{v=$NF;p=index(v,"-");r=1;if(p){v=substr(v,1,p-1);r=0};split(v,a,".");printf "%d %d %d %d %s\n",a[1],a[2],a[3],r,$0}' | sort -k1,1nr -k2,2nr -k3,3nr -k4,4nr | cut -d' ' -f5-)"
|
|
84
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
85
|
+
# '${CLAUDE_PLUGIN_ROOT}' is substituted by Claude Code when it loads a plugin spec (the exact spelling only, never as an env var), so a running plugin resolves to its own root first; unsubstituted (other runtimes) it stays a literal that the /* check rejects.
|
|
86
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
87
|
+
while IFS= read -r _c; do
|
|
88
|
+
case "$_c" in /*) [ -z "$FORGE_ROOT" ] && [ -f "$_c/scripts/verify-phase-trail.sh" ] && [ -f "$_c/scripts/lint-dispatch-prompt.sh" ] && [ -f "$_c/scripts/is-docs-only.sh" ] && [ -f "$_c/bin/engine/resolve.mjs" ] && [ -f "$_c/bin/engine/orchestrate-canary.mjs" ] && [ -f "$_c/bin/engine/admission.mjs" ] && FORGE_ROOT="$_c" ;; esac
|
|
89
|
+
done <<< "$_k"
|
|
90
|
+
fi
|
|
91
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
92
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
93
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
94
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
95
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
96
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
97
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
98
|
+
# which is always safe and available. A missing script is never a reason
|
|
99
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
100
|
+
|
|
101
|
+
resolve_script() {
|
|
102
|
+
local operation="$1"
|
|
103
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
104
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
105
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
106
|
+
return
|
|
107
|
+
fi
|
|
108
|
+
# Tier 3: universal script
|
|
109
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
110
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
111
|
+
return
|
|
112
|
+
fi
|
|
113
|
+
# Tier 4: prose fallback
|
|
114
|
+
echo "prose:"
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
|
|
32
119
|
## Phase 0.5: Memory Retrieval — Prior Run Priors <!-- Added: forge#1316 -->
|
|
33
120
|
|
|
34
121
|
**Goal**: Before investigating, retrieve the top-k relevant prior pipeline runs from the per-repo memory index. Inject confirmed priors into the investigation context so the pipeline compounds intelligence across runs.
|
|
@@ -257,25 +344,124 @@ Store `RECALL_ISSUE_CITATIONS` in a variable — it is used in Phase 1C to popul
|
|
|
257
344
|
|
|
258
345
|
## Phase 1A: Load Issue & Check Resume State
|
|
259
346
|
|
|
347
|
+
Re-read all state from GitHub (this fork has no inherited context):
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Resume check** — run this FIRST (before Phase 0.5):
|
|
354
|
+
|
|
260
355
|
```bash
|
|
261
|
-
gh
|
|
262
|
-
|
|
356
|
+
COMMENTS_JSON=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate --jq '.[] | {id: .id, url: .html_url, body: .body}' 2>/dev/null | jq -s '.' 2>/dev/null)
|
|
357
|
+
[ -z "$COMMENTS_JSON" ] && COMMENTS_JSON='[]'
|
|
358
|
+
|
|
359
|
+
# Complete = INVESTIGATOR header AND (COMPLETE or INVALID sentinel) in the SAME comment. Use the last such comment.
|
|
360
|
+
DONE_JSON=$(echo "$COMMENTS_JSON" | jq -c '[.[] | select(.body | contains("<!-- FORGE:INVESTIGATOR -->"))
|
|
361
|
+
| select((.body | contains("<!-- INVESTIGATION:COMPLETE -->")) or (.body | contains("<!-- INVESTIGATION:INVALID -->")))] | last // empty')
|
|
362
|
+
|
|
363
|
+
# Partial = INVESTIGATOR header but NEITHER sentinel (interrupted run)
|
|
364
|
+
PARTIAL_IDS=$(echo "$COMMENTS_JSON" | jq -r '.[] | select(.body | contains("<!-- FORGE:INVESTIGATOR -->"))
|
|
365
|
+
| select((.body | contains("<!-- INVESTIGATION:COMPLETE -->") | not) and (.body | contains("<!-- INVESTIGATION:INVALID -->") | not)) | .id')
|
|
366
|
+
|
|
367
|
+
if [ -n "$DONE_JSON" ]; then
|
|
368
|
+
DONE_BODY=$(echo "$DONE_JSON" | jq -r '.body')
|
|
369
|
+
DONE_URL=$(echo "$DONE_JSON" | jq -r '.url')
|
|
370
|
+
R_VERDICT=$(printf '%s\n' "$DONE_BODY" | grep -oE '\*\*Verdict\*\*: *[A-Za-z_-]+' | head -1 | sed -E 's/.*: *//')
|
|
371
|
+
R_CONF=$(printf '%s\n' "$DONE_BODY" | grep -oE '\*\*Confidence\*\*: *[A-Za-z]+' | head -1 | sed -E 's/.*: *//')
|
|
372
|
+
if printf '%s\n' "$DONE_BODY" | sed -n '/^### Decomposition Assessment/,/^###[^#]/p' | grep -qE '^\*\*YES\*\*'; then R_DECOMP=YES; else R_DECOMP=NO; fi
|
|
373
|
+
[ "$R_VERDICT" = "INVALID" ] && R_STATUS=INVALID || R_STATUS=ALREADY_DONE
|
|
374
|
+
R_GIST=$(echo "$COMMENTS_JSON" | jq -r '[.[] | .body | select(contains("FORGE:KNOWLEDGE_GIST:"))] | first // ""' | grep -oE 'https://gist[^ ]+' | head -1)
|
|
375
|
+
R_MS_NUM=$(gh issue view {NUMBER} {GH_FLAG} --json milestone --jq '.milestone.number // empty' 2>/dev/null)
|
|
376
|
+
R_MS_IDX=""
|
|
377
|
+
[ -n "$R_MS_NUM" ] && R_MS_IDX=$(gh api "repos/{GH_REPO}/milestones/${R_MS_NUM}" --jq '.description // ""' 2>/dev/null | grep -oE '<!-- FORGE:MILESTONE_INDEX: https://[^ ]+ -->' | head -1 | sed -E 's/^<!-- FORGE:MILESTONE_INDEX: //; s/ -->$//')
|
|
378
|
+
echo "INVESTIGATE_RESULT:"
|
|
379
|
+
echo " status: $R_STATUS"
|
|
380
|
+
echo " verdict: ${R_VERDICT:-null}"
|
|
381
|
+
echo " confidence: ${R_CONF:-null}"
|
|
382
|
+
echo " decompose: $R_DECOMP"
|
|
383
|
+
echo " comment_url: $DONE_URL"
|
|
384
|
+
echo " gist_url: ${R_GIST:-null}"
|
|
385
|
+
echo " milestone_index_url: ${R_MS_IDX:-null}"
|
|
386
|
+
echo " blocker: null"
|
|
387
|
+
echo "RESUME: investigation already complete — copy the block above verbatim as your final reply and STOP."
|
|
388
|
+
else
|
|
389
|
+
for CID in $PARTIAL_IDS; do
|
|
390
|
+
gh api "repos/{GH_REPO}/issues/comments/${CID}" -X DELETE 2>/dev/null || true # interrupted investigation: delete partial comment and restart
|
|
391
|
+
done
|
|
392
|
+
echo "RESUME: no completed investigation — proceeding with a fresh run."
|
|
393
|
+
fi
|
|
263
394
|
```
|
|
264
395
|
|
|
265
|
-
**Resume logic
|
|
266
|
-
-
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
396
|
+
**Resume logic** (what the block implements):
|
|
397
|
+
- `<!-- FORGE:INVESTIGATOR -->` comment AND (`<!-- INVESTIGATION:COMPLETE -->` OR `<!-- INVESTIGATION:INVALID -->`) in the SAME comment → investigation already complete. Print the parsed existing verdict as the `INVESTIGATE_RESULT:` block (`status: ALREADY_DONE`, or `status: INVALID` when the existing verdict is INVALID) and **STOP** — do not run any later phase. `INVESTIGATION:INVALID` is the terminal sentinel Phase 1C emits for an INVALID verdict (see Phase 1C) — it is just as much a completion marker as `INVESTIGATION:COMPLETE`.
|
|
398
|
+
- `<!-- FORGE:INVESTIGATOR -->` comment but NEITHER sentinel → investigation was interrupted; the partial comment(s) are deleted and the run restarts.
|
|
399
|
+
- No investigator comment → fresh investigation.
|
|
400
|
+
|
|
401
|
+
**Set label** (only reached when not ALREADY_DONE). Use the tiered transition script; the prose fallback adds `workflow:investigating` and removes every other `workflow:*` state label:
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
405
|
+
case "$TIER" in
|
|
406
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
407
|
+
prose)
|
|
408
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:investigating" --remove-label "workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
409
|
+
;;
|
|
410
|
+
esac
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### 1A.5: Normalize Issue Body (MANDATORY)
|
|
416
|
+
|
|
417
|
+
Before investigation begins, verify the issue body contains the four mandatory pipeline sections. If any are missing, add placeholder content so the investigator has the correct scaffolding.
|
|
418
|
+
|
|
419
|
+
**Skip if**: All four sections (`## Problem`, `## Affected Files`, `## Expected Behavior`, `## Acceptance Criteria`) are already present.
|
|
272
420
|
|
|
273
|
-
**Set label**:
|
|
274
421
|
```bash
|
|
275
|
-
gh issue
|
|
276
|
-
|
|
422
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
423
|
+
|
|
424
|
+
MISSING_SECTIONS=""
|
|
425
|
+
echo "$ISSUE_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
|
|
426
|
+
echo "$ISSUE_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
|
|
427
|
+
echo "$ISSUE_BODY" | grep -q "^## Expected Behavior" || MISSING_SECTIONS="$MISSING_SECTIONS EXPECTED_BEHAVIOR"
|
|
428
|
+
echo "$ISSUE_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
|
|
429
|
+
|
|
430
|
+
if [ -n "$MISSING_SECTIONS" ]; then
|
|
431
|
+
echo "Missing sections:$MISSING_SECTIONS — normalizing issue body before investigation"
|
|
432
|
+
|
|
433
|
+
APPEND_TEXT=""
|
|
434
|
+
echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
|
|
435
|
+
## Problem
|
|
436
|
+
|
|
437
|
+
Root cause unknown — investigation needed."
|
|
438
|
+
|
|
439
|
+
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
440
|
+
## Affected Files
|
|
441
|
+
|
|
442
|
+
Files to be identified during investigation."
|
|
443
|
+
|
|
444
|
+
echo "$MISSING_SECTIONS" | grep -q "EXPECTED_BEHAVIOR" && APPEND_TEXT="$APPEND_TEXT
|
|
445
|
+
## Expected Behavior
|
|
446
|
+
|
|
447
|
+
Expected behavior to be determined during investigation."
|
|
448
|
+
|
|
449
|
+
echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
|
|
450
|
+
## Acceptance Criteria
|
|
451
|
+
|
|
452
|
+
- [ ] Fix confirmed during investigation."
|
|
453
|
+
|
|
454
|
+
# Append missing sections to the existing body (never replace — only extend)
|
|
455
|
+
NORMALIZED_BODY="${ISSUE_BODY}${APPEND_TEXT}"
|
|
456
|
+
gh issue edit {NUMBER} {GH_FLAG} --body "$NORMALIZED_BODY" # allowlist:check-command-side-effects
|
|
457
|
+
echo "Issue body normalized — added:$MISSING_SECTIONS"
|
|
458
|
+
else
|
|
459
|
+
echo "Issue body already contains all mandatory sections — skipping normalization"
|
|
460
|
+
fi
|
|
277
461
|
```
|
|
278
462
|
|
|
463
|
+
**Continue to Phase 1B unconditionally.** Normalization is a compensation step — it never blocks investigation.
|
|
464
|
+
|
|
279
465
|
---
|
|
280
466
|
|
|
281
467
|
## Phase 1B: Investigate
|
|
@@ -293,6 +479,14 @@ The target repo is `{GH_REPO}` (resolved from `forge.yaml → project`). The wor
|
|
|
293
479
|
- Infrastructure: `.github/workflows/`, `docker-compose*.yml`, `infra/`
|
|
294
480
|
- Config: `forge.yaml`, `.env.example`, any `config/` directory
|
|
295
481
|
|
|
482
|
+
Read `forge.yaml → review.tech_stack` and `forge.yaml → review.key_paths` (if present) to identify which files are most relevant for the affected domain. If `review.key_paths` lists domain-to-file mappings, use that table directly. If the `review` section is absent, use the issue labels, title keywords, and the affected files listed in the issue body to determine the domain. Start with the files the issue explicitly names, then expand to callers and related modules.
|
|
483
|
+
|
|
484
|
+
**Workflow pipeline issues** (repo is a ForgeDock installation):
|
|
485
|
+
- Key files: `commands/work-on.md`, `commands/review-pr.md`, `commands/quality-gate.md`, `commands/orchestrate.md`, `forge.yaml`, `bin/forgedock.mjs`
|
|
486
|
+
|
|
487
|
+
**INFRA domain known footguns** (read before writing any `.github/workflows/*.yml` changes):
|
|
488
|
+
- **appleboy/ssh-action Go template preprocessing**: Any `{{` in a `script:` block is interpreted as a Go template directive **before the script reaches SSH**. This means `docker ps --format '{{.Names}}'` and `docker inspect --format '{{index .RepoTags 0}}'` will crash the action with exit 1. Both function calls (`{{index .X Y}}`) AND field accessors (`{{.Names}}`, `{{.Status}}`) fail on the action's empty data context. Shell error handlers (`|| fallback`, `set -e`, `2>/dev/null`) are bypassed because the failure is client-side. Always use `docker inspect IMAGE | jq -r '.[0].RepoTags[0]'` and `docker ps --format json | jq -r '.Names'` patterns in `appleboy/ssh-action` scripts. (Ref: forge#226 — 6-day silent deploy failure masked by `continue-on-error: true`)
|
|
489
|
+
|
|
296
490
|
If the issue specifies a **Code branch** (`**Code branch**: \`{branch}\``), check out that branch — the affected files may not be on the default branch.
|
|
297
491
|
|
|
298
492
|
### Code Index Query (run BEFORE any grep exploration)
|
|
@@ -322,7 +516,22 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
322
516
|
|
|
323
517
|
1. **Check the right branch** — read from the branch specified in the issue body (`**Code branch**: \`{branch}\``) if present
|
|
324
518
|
2. **Read domain files** — start with the key files for the affected domain (use index query results from Step 0E as the file list; fall back to directory inspection if index is absent)
|
|
519
|
+
2.5. **Existing system search (conditional, MUST)**: If the issue describes a gap in a functional capability — content not being distributed, notifications not sending, jobs not running, data not being synced — MUST search for an existing automated system before proposing a new one. The issue body may name a specific tool or path (e.g., `reddit-bot/`, `marketing/`) — do NOT anchor on that path alone. Expand the search to all service layers:
|
|
520
|
+
```bash
|
|
521
|
+
# Check all service layers for the capability (adapt paths to your project structure)
|
|
522
|
+
grep -rn "{capability_keyword}" {REPO_PATH}/services/ --include="*.py" -l | head -20
|
|
523
|
+
# Look for scheduled jobs, automated runners, existing integrations
|
|
524
|
+
grep -rn "scheduler\|celery\|cron\|nightly\|periodic" {REPO_PATH}/services/ --include="*.py" -l | head -10
|
|
525
|
+
```
|
|
526
|
+
If an existing system is found that already handles the capability: the fix MUST route through the existing system (fix its config, env var, or gate) — NOT create a new parallel tool. Document the existing system in the investigation report and make it the centerpiece of the recommendation. This check is especially critical when the issue references a standalone tool directory (`reddit-bot/`, `scripts/`, `tools/`) — those directories often duplicate functionality that a service already owns. (Ref: forge#279 — investigator anchored on `reddit-bot/` from issue body, never checked `services/herald/app/scheduler/`, built parallel PRAW integration alongside Herald's existing automated crosspost scheduler)
|
|
325
527
|
3. **Verify claims** — does the code actually have the problem described?
|
|
528
|
+
3.5. **Type Invariant Verification (MANDATORY)**: Before declaring that a field, key, or parameter has a specific type (e.g. "content is always a dict", "status is always an int"), search for ALL code paths that write to that field across ALL services:
|
|
529
|
+
```bash
|
|
530
|
+
grep -rn '"field_name"\s*:' services/ # Python dict key assignments
|
|
531
|
+
grep -rn 'result\["field_name"\]\s*=' services/ # Direct assignments
|
|
532
|
+
grep -rn '\.field_name\s*=' services/ # Attribute assignments
|
|
533
|
+
```
|
|
534
|
+
If the field is written with different types in different code paths (e.g. dict in the standard path, string in the auth-gated path), document ALL variants. The fix must handle every variant — not just the one on the primary investigated code path. A type guard like `or {}` only protects against falsy values; a non-empty string is truthy and bypasses it.
|
|
326
535
|
4. **Git blame** — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
|
|
327
536
|
```bash
|
|
328
537
|
# Introducing commit for each affected file (first commit that added it)
|
|
@@ -333,9 +542,10 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
333
542
|
git blame -L {start},{end} -- {affected_file}
|
|
334
543
|
```
|
|
335
544
|
Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
|
|
545
|
+
4.5. **Rogue commit pre-state comparison (conditional)**: If the issue body references a specific commit as rogue, bad, or unintended (e.g., "rogue commit `abc1234`", "bad commit", "this was never intended"), MUST run `git show {commit}^:{file}` to see the file before that commit. Compare the pre-commit state against the current file. Any block present in the current file but absent in the pre-commit state was introduced by that commit chain and is a candidate for full reversion — not just partial editing. Report the delta (pre vs. current) in the investigation report. Do NOT assume surrounding code near a named import/bug is correct simply because the issue only named a specific sub-problem. (Ref: forge#278 — investigator confirmed the broken import but never ran `git show 18a3a2cf3^:batch.py`; the surrounding 50-line feature gate was also rogue and was preserved by the fix PR, causing a P1 access regression for all non-Scale users)
|
|
336
546
|
5. **Domain context discovery** (narrow scope only, 1–5 files):
|
|
337
547
|
```bash
|
|
338
|
-
git log --oneline --all -30 -- {affected_files} | grep -
|
|
548
|
+
git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
|
|
339
549
|
gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
|
|
340
550
|
```
|
|
341
551
|
Keep only file/function-level overlap. Max 5 related issues. Everything is a hint to verify, not a fact.
|
|
@@ -349,6 +559,17 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
349
559
|
Any hit here is a candidate prior fix or reintroduced defect — read the commit body (`git show {hash}`) to confirm before citing it. Feed confirmed hits into the History findings field and let them inform the verdict (e.g. a defect being reintroduced raises severity).
|
|
350
560
|
6. **Determine root cause** — what's actually broken or missing?
|
|
351
561
|
7. **Identify affected files** — full list of files that need changes
|
|
562
|
+
7.5. **Sibling Pattern Sweep** *(conditional — when the bug is a condition, gated function call, or field presence check)*: After identifying the affected files, grep for the same pattern in sibling files within the same directory. The issue spec may name only the file where the error was first observed — but the same commit or PR that introduced the bug often applied it uniformly across related handlers.
|
|
563
|
+
```bash
|
|
564
|
+
# Identify the broken condition or gated function call from the issue
|
|
565
|
+
# Then search sibling files in the same router/service directory
|
|
566
|
+
AFFECTED_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
|
|
567
|
+
grep -rn "{broken_pattern}" "$AFFECTED_DIR" --include="*.py" | grep -v "{PRIMARY_AFFECTED_FILE}"
|
|
568
|
+
```
|
|
569
|
+
**If identical patterns are found in files NOT listed in the issue spec**, output a scope-gap warning:
|
|
570
|
+
> **Scope-Gap Warning**: The issue spec lists `{PRIMARY_FILE}` but the same pattern exists in `{SIBLING_FILE}:{LINE}`. These were likely introduced together. Recommend widening scope to fix all callers in this PR, or creating follow-up issues for the other files before proceeding.
|
|
571
|
+
|
|
572
|
+
Do NOT silently exclude sibling matches. The appropriate output when sibling files have the same bug is to flag them explicitly — even if the issue spec's silence appears intentional. The fix-approach validation step (step 8) will confirm whether to widen scope or create follow-ups. <!-- Added: forge#383 -->
|
|
352
573
|
8. **Fix-approach validation** — if the issue proposes a fix, don't adopt it as spec. Trace through the target system's middleware, auth, routing, config. Cross-domain: if fix in domain A interacts with domain B, read domain B's files too.
|
|
353
574
|
|
|
354
575
|
---
|
|
@@ -458,7 +679,7 @@ ACCEPTANCE_CHECK: id=ac-4 type=command target="grep -qE '(>= ?2|2\+)' commands/o
|
|
|
458
679
|
|
|
459
680
|
**Check types**:
|
|
460
681
|
- `exists` — assert a file or directory exists (`target` = path, `matcher` = ignored)
|
|
461
|
-
- `contains` — assert a file contains a string or regex (`target` = file path, `matcher` = string/regex)
|
|
682
|
+
- `contains` — assert a file contains a string or regex (`target` = file path, `matcher` = string/regex). **Prefer a literal string** for code: the matcher is applied as an extended regex first, so `$`, `(`, `[`, `.`, `*`, `+`, `?`, `|` must be escaped if you mean them literally (e.g. `return \"\$HELD\"`). The acceptance gate falls back to a literal match, but a correct matcher avoids a needless repair round. For a `command` check that looks for code, use `grep -qF '<literal>' <file>`, not `grep -qE`.
|
|
462
683
|
- `command` — run a shell command and assert exit 0 (`target` = shell command, `matcher` = `exit_0`)
|
|
463
684
|
- `behavior` — assert a runtime/observable behavior via shell command (`target` = shell command, `matcher` = expected output string or regex)
|
|
464
685
|
|
|
@@ -489,12 +710,15 @@ own process. GitHub App installation identities are `Bot` users and cannot use t
|
|
|
489
710
|
|
|
490
711
|
```bash
|
|
491
712
|
if [ -z "${FORGE_GIST_CAPABLE+x}" ]; then
|
|
713
|
+
# Fail closed: only a positively identified user account can use the Gists API. A GitHub App
|
|
714
|
+
# installation token cannot read /user at all (HTTP 403, empty type), so "not Bot" is NOT
|
|
715
|
+
# evidence of capability; an unavailable probe disables Gists rather than letting every
|
|
716
|
+
# worker fail on its first Gist write.
|
|
492
717
|
GIST_AUTH_TYPE=$(gh api user --jq '.type' 2>/dev/null || true)
|
|
493
|
-
if [ "$GIST_AUTH_TYPE" = "
|
|
494
|
-
FORGE_GIST_CAPABLE=false
|
|
495
|
-
else
|
|
496
|
-
# Preserve the existing behavior when the identity probe is unavailable or is a PAT user.
|
|
718
|
+
if [ "$GIST_AUTH_TYPE" = "User" ]; then
|
|
497
719
|
FORGE_GIST_CAPABLE=true
|
|
720
|
+
else
|
|
721
|
+
FORGE_GIST_CAPABLE=false
|
|
498
722
|
fi
|
|
499
723
|
export FORGE_GIST_CAPABLE
|
|
500
724
|
fi
|
|
@@ -550,10 +774,10 @@ GIST_FILENAME="${REPO_SHORT}_${NUMBER}_${SLUG}.md"
|
|
|
550
774
|
Extract verdict, task type, and confidence from the investigation body, then compose the Gist:
|
|
551
775
|
|
|
552
776
|
```bash
|
|
553
|
-
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -
|
|
554
|
-
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -
|
|
555
|
-
CONFIDENCE=$(echo "$INVESTIGATION_BODY" | grep -
|
|
556
|
-
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -
|
|
777
|
+
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Verdict\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Verdict\*\*: //')
|
|
778
|
+
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Task Type\*\*: .+' | head -1 | sed -E 's/^\*\*Task Type\*\*: //')
|
|
779
|
+
CONFIDENCE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Confidence\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Confidence\*\*: //')
|
|
780
|
+
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Severity\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Severity\*\*: //')
|
|
557
781
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
558
782
|
|
|
559
783
|
GIST_CONTENT=$(cat <<GIST_EOF
|
|
@@ -637,16 +861,16 @@ fi
|
|
|
637
861
|
|
|
638
862
|
```bash
|
|
639
863
|
MILESTONE_DESC=$(gh api repos/{GH_REPO}/milestones/${MILESTONE_NUM} --jq '.description // ""')
|
|
640
|
-
EXISTING_INDEX_URL=$(echo "$MILESTONE_DESC" | grep -
|
|
864
|
+
EXISTING_INDEX_URL=$(echo "$MILESTONE_DESC" | grep -oE '<!-- FORGE:MILESTONE_INDEX: https://[^ ]+ -->' | head -1 | sed -E 's/^<!-- FORGE:MILESTONE_INDEX: //; s/ -->$//')
|
|
641
865
|
```
|
|
642
866
|
|
|
643
867
|
### Step 3: Build index entry for this issue
|
|
644
868
|
|
|
645
869
|
```bash
|
|
646
870
|
ISSUE_TITLE=$(gh issue view {NUMBER} {GH_FLAG} --json title --jq '.title')
|
|
647
|
-
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -
|
|
648
|
-
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -
|
|
649
|
-
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -
|
|
871
|
+
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Verdict\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Verdict\*\*: //')
|
|
872
|
+
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Severity\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Severity\*\*: //')
|
|
873
|
+
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Task Type\*\*: .+' | head -1 | sed -E 's/^\*\*Task Type\*\*: //')
|
|
650
874
|
RECOMMENDATION=$(echo "$INVESTIGATION_BODY" | sed -n '/^### Recommendation/,/^### /p' | head -5 | tail -n +2 | tr '\n' ' ' | cut -c1-120)
|
|
651
875
|
|
|
652
876
|
# GIST_URL comes from Phase 1C.5 (may be empty if Gist creation failed)
|
|
@@ -697,7 +921,7 @@ fi
|
|
|
697
921
|
|
|
698
922
|
```bash
|
|
699
923
|
if [ -n "$EXISTING_INDEX_URL" ]; then
|
|
700
|
-
INDEX_GIST_ID=$(echo "$EXISTING_INDEX_URL" | grep -
|
|
924
|
+
INDEX_GIST_ID=$(echo "$EXISTING_INDEX_URL" | grep -oE '[a-f0-9]{20,}' | tail -1)
|
|
701
925
|
|
|
702
926
|
if [ -z "$INDEX_GIST_ID" ]; then
|
|
703
927
|
echo "WARNING: Could not extract Gist ID from index URL — skipping update"
|
|
@@ -733,7 +957,7 @@ ${INDEX_ENTRY}"
|
|
|
733
957
|
|
|
734
958
|
# Update the Gist
|
|
735
959
|
# gh gist edit does not support stdin via '-'; use a temp file instead
|
|
736
|
-
TMPFILE=$(mktemp
|
|
960
|
+
TMPFILE=$(mktemp "${TMPDIR:-/tmp}/forge-index.XXXXXX")
|
|
737
961
|
echo "$UPDATED_INDEX" > "$TMPFILE"
|
|
738
962
|
gh gist edit "$INDEX_GIST_ID" -f "$INDEX_FILENAME" "$TMPFILE" 2>/dev/null
|
|
739
963
|
EDIT_EXIT=$?
|
|
@@ -822,58 +1046,150 @@ fi
|
|
|
822
1046
|
|
|
823
1047
|
---
|
|
824
1048
|
|
|
825
|
-
|
|
1049
|
+
### 1D.1: Correction Capture (MANDATORY — run after 1D.0, BEFORE the workflow label update) <!-- Added: forge#667 -->
|
|
1050
|
+
|
|
1051
|
+
Before routing, scan all non-agent comments for correction signals from the repository owner. Correction signals are owner comments that contain phrases like "no, use", "actually use", "use X instead", "not X, use Y", or "wrong branch". If found, write the correction to `forge.yaml → learned:` and emit a `FORGE:LEARNED` annotation.
|
|
1052
|
+
|
|
1053
|
+
**Scan for correction signals**:
|
|
826
1054
|
```bash
|
|
827
|
-
|
|
1055
|
+
# Get repo owner login for filtering.
|
|
1056
|
+
# Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
|
|
1057
|
+
# but comment .user.login is always a personal account login. For org-owned repos
|
|
1058
|
+
# these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
|
|
1059
|
+
# so using project.owner directly silently disables correction capture for all org repos.
|
|
1060
|
+
#
|
|
1061
|
+
# Resolution order:
|
|
1062
|
+
# 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
|
|
1063
|
+
# 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
|
|
1064
|
+
# 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
|
|
1065
|
+
REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
|
|
1066
|
+
if [ -z "$REPO_OWNER" ]; then
|
|
1067
|
+
REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
|
|
1068
|
+
fi
|
|
1069
|
+
if [ -z "$REPO_OWNER" ]; then
|
|
1070
|
+
REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
|
|
1071
|
+
fi
|
|
1072
|
+
|
|
1073
|
+
# Fetch all comments, filter to owner-only, look for correction signals
|
|
1074
|
+
CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1075
|
+
| jq -r --arg owner "$REPO_OWNER" \
|
|
1076
|
+
'.[] | select(.user.login == $owner) | select(
|
|
1077
|
+
(.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
|
|
1078
|
+
) | .body' 2>/dev/null || echo '')
|
|
828
1079
|
```
|
|
829
1080
|
|
|
830
|
-
|
|
1081
|
+
**If correction signals found** — extract and write each correction:
|
|
1082
|
+
|
|
831
1083
|
```bash
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1084
|
+
# Example: extract branch target correction "use develop not staging"
|
|
1085
|
+
# Adjust regex to the correction pattern detected
|
|
1086
|
+
|
|
1087
|
+
if [ -n "$CORRECTIONS" ]; then
|
|
1088
|
+
echo "Correction signals detected — writing to forge.yaml → learned:"
|
|
1089
|
+
echo "$CORRECTIONS"
|
|
1090
|
+
|
|
1091
|
+
# Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
|
|
1092
|
+
# Always use env variable injection to avoid YAML injection from comment content
|
|
1093
|
+
# Example for branch target correction:
|
|
1094
|
+
# BRANCH_VALUE="develop"
|
|
1095
|
+
# yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
|
|
1096
|
+
|
|
1097
|
+
# After writing, emit FORGE:LEARNED annotation
|
|
1098
|
+
LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
|
|
1099
|
+
CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1100
|
+
|
|
1101
|
+
# Update captured_at and captured_by metadata
|
|
1102
|
+
CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
|
|
1103
|
+
CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
|
|
1104
|
+
|
|
1105
|
+
LEARNED_BODY="<!-- FORGE:LEARNED -->
|
|
1106
|
+
## Learned Pattern Captured
|
|
1107
|
+
|
|
1108
|
+
**Source**: Owner correction in comment on issue #{NUMBER}
|
|
1109
|
+
**Captured at**: $CAPTURED_AT
|
|
1110
|
+
**Keys written**: \`$LEARNED_KEYS\`
|
|
1111
|
+
|
|
1112
|
+
The following project-specific pattern was detected from owner feedback and written to \`forge.yaml → learned:\`. Future sessions will use this override automatically.
|
|
1113
|
+
|
|
1114
|
+
\`\`\`yaml
|
|
1115
|
+
# Written to forge.yaml
|
|
1116
|
+
learned:
|
|
1117
|
+
# {key}: {value}
|
|
1118
|
+
\`\`\`
|
|
1119
|
+
|
|
1120
|
+
**Idempotency**: yq merge-write — re-running will not duplicate entries."
|
|
1121
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$LEARNED_BODY" # allowlist:check-command-side-effects
|
|
1122
|
+
|
|
1123
|
+
echo "FORGE:LEARNED annotation posted."
|
|
1124
|
+
fi
|
|
837
1125
|
```
|
|
838
1126
|
|
|
839
|
-
|
|
1127
|
+
**Idempotency guarantee**: Use `yq eval '.learned.key = env(VAR)' -i forge.yaml` — yq overwrites existing keys rather than appending. Re-running the capture step on the same comment produces the same forge.yaml state.
|
|
1128
|
+
|
|
1129
|
+
---
|
|
1130
|
+
|
|
1131
|
+
### 1D.2: Update Labels & Return Verdict
|
|
1132
|
+
|
|
1133
|
+
Resolve the posted investigation comment URL once (used in the result block):
|
|
840
1134
|
|
|
841
|
-
**CONFIRMED or PARTIAL with decompose: YES**:
|
|
842
1135
|
```bash
|
|
843
|
-
gh
|
|
1136
|
+
COMMENT_URL=$(gh api "repos/{GH_REPO}/issues/{NUMBER}/comments" --paginate --jq '[.[] | select(.body | contains("<!-- FORGE:INVESTIGATOR -->")) | .html_url] | last' 2>/dev/null | tail -1)
|
|
844
1137
|
```
|
|
845
|
-
Do NOT add `workflow:ready-to-build` — the routing loop will invoke `work-on:decompose` based on the `decompose: YES` return value.
|
|
846
1138
|
|
|
847
|
-
|
|
1139
|
+
**CONFIRMED or PARTIAL with decompose: NO** — transition to `ready-to-build` (this phase owns that label):
|
|
848
1140
|
```bash
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1141
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1142
|
+
case "$TIER" in
|
|
1143
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
1144
|
+
prose)
|
|
1145
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:ready-to-build" --remove-label "workflow:investigating,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:invalid,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1146
|
+
;;
|
|
1147
|
+
esac
|
|
854
1148
|
```
|
|
855
1149
|
|
|
856
|
-
|
|
1150
|
+
No `FORGE:CHECKPOINT` is written — the label already disambiguates the resume point (forge#1826). Print `INVESTIGATE_RESULT:` with `status: COMPLETE`, `decompose: NO` and STOP.
|
|
857
1151
|
|
|
858
|
-
**
|
|
1152
|
+
**CONFIRMED or PARTIAL with decompose: YES** — this phase does NOT set `workflow:decomposed` (the decompose phase owns it) and does NOT add `workflow:ready-to-build`. Only remove the investigating label:
|
|
859
1153
|
```bash
|
|
860
|
-
gh issue edit {NUMBER} {GH_FLAG} --
|
|
861
|
-
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
|
|
1154
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
862
1155
|
```
|
|
863
|
-
|
|
1156
|
+
|
|
1157
|
+
No `FORGE:CHECKPOINT` is written. Print `INVESTIGATE_RESULT:` with `status: COMPLETE`, `decompose: YES` and STOP.
|
|
1158
|
+
|
|
1159
|
+
**INVALID** — transition to `invalid` and close the issue (terminal):
|
|
1160
|
+
```bash
|
|
1161
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1162
|
+
case "$TIER" in
|
|
1163
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
1164
|
+
prose)
|
|
1165
|
+
gh issue edit {NUMBER} {GH_FLAG} --add-label "workflow:invalid" --remove-label "workflow:investigating,workflow:ready-to-build,workflow:building,workflow:in-review,workflow:awaiting-merge,workflow:merged,workflow:decomposed" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
1166
|
+
;;
|
|
1167
|
+
esac
|
|
1168
|
+
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}" # allowlist:check-command-side-effects
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
No checkpoint written — INVALID is terminal. Print `INVESTIGATE_RESULT:` with `status: INVALID` and STOP.
|
|
864
1172
|
|
|
865
1173
|
---
|
|
866
1174
|
|
|
867
1175
|
## Output
|
|
868
1176
|
|
|
869
|
-
The subcommand writes its results to GitHub (FORGE:INVESTIGATOR comment).
|
|
1177
|
+
The subcommand writes its results to GitHub (FORGE:INVESTIGATOR comment, labels). Its **final reply** must be exactly this one block — it is all the caller sees. Print it on **every** exit path (COMPLETE, ALREADY_DONE, INVALID, BLOCKED). This subcommand is complete after printing; do not continue to any later phase.
|
|
870
1178
|
|
|
871
1179
|
```
|
|
872
1180
|
INVESTIGATE_RESULT:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1181
|
+
status: {COMPLETE|ALREADY_DONE|INVALID|BLOCKED}
|
|
1182
|
+
verdict: {CONFIRMED|PARTIAL|INVALID|null}
|
|
1183
|
+
confidence: {HIGH|MEDIUM|LOW|null}
|
|
1184
|
+
decompose: {YES|NO|null}
|
|
1185
|
+
comment_url: {url of posted (or pre-existing) comment, or null}
|
|
877
1186
|
gist_url: {url of knowledge gist, or null if creation failed/skipped}
|
|
878
1187
|
milestone_index_url: {url of milestone index gist, or null if no milestone/creation failed}
|
|
1188
|
+
blocker: {one-line reason when status is BLOCKED, else null}
|
|
879
1189
|
```
|
|
1190
|
+
|
|
1191
|
+
**Status mapping**:
|
|
1192
|
+
- `COMPLETE` — fresh investigation finished with verdict CONFIRMED or PARTIAL (`decompose` YES or NO).
|
|
1193
|
+
- `ALREADY_DONE` — resume check found a completed investigation with a non-INVALID verdict; the block carries the parsed existing verdict (Phase 1A).
|
|
1194
|
+
- `INVALID` — verdict INVALID (fresh run, or an existing `INVESTIGATION:INVALID` comment found on resume); issue closed with `workflow:invalid`.
|
|
1195
|
+
- `BLOCKED` — a required arg is missing, the issue cannot be read, or investigation cannot proceed; set `blocker` and leave the other fields `null` where unknown.
|