forgedock 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/design-bench.md +1 -0
- package/commands/design-render-critique-loop.md +1 -0
- package/commands/design.md +1 -0
- package/commands/forge-stats.md +1 -0
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +3 -2
- package/commands/orchestrate/phase-1-resolve.md +57 -12
- package/commands/orchestrate/phase-2-triage.md +1 -0
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -0
- package/commands/orchestrate/phase-3-dependency.md +17 -5
- package/commands/orchestrate/phase-4-execution.md +625 -78
- package/commands/orchestrate/phase-5-cleanup.md +5 -2
- package/commands/orchestrate/phase-6-report.md +8 -3
- package/commands/orchestrate/safety.md +1 -0
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/config.md +1 -0
- package/commands/pipeline-health/phase-1-context.md +1 -0
- package/commands/pipeline-health/phase-2-metrics.md +1 -0
- package/commands/pipeline-health/phase-3-analyze.md +1 -0
- package/commands/pipeline-health/phase-4-proposals.md +7 -4
- package/commands/pipeline-health/phase-5-report.md +5 -2
- package/commands/pipeline-health/phase-6-summary.md +1 -0
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +219 -13
- package/commands/review-pr-agents/api.md +1 -0
- package/commands/review-pr-agents/auth.md +1 -0
- package/commands/review-pr-agents/billing.md +1 -0
- package/commands/review-pr-agents/concurrency.md +1 -0
- package/commands/review-pr-agents/database.md +1 -0
- package/commands/review-pr-agents/frontend.md +1 -0
- package/commands/review-pr-agents/infra.md +1 -0
- package/commands/review-pr-agents/protocols.md +1 -0
- package/commands/review-pr-agents/scraper.md +1 -0
- package/commands/review-pr-agents/security.md +1 -0
- package/commands/review-pr-agents/spec-cli.md +1 -0
- package/commands/review-pr-agents.md +10 -4
- package/commands/review-pr-staging.md +14 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +3 -1
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +82 -19
- package/commands/work-on/build/context.md +62 -18
- package/commands/work-on/build/implement.md +63 -18
- package/commands/work-on/build/validate.md +241 -60
- package/commands/work-on/build.md +345 -146
- package/commands/work-on/close.md +618 -246
- package/commands/work-on/decompose.md +240 -41
- package/commands/work-on/investigate.md +378 -61
- package/commands/work-on/remediate.md +43 -9
- package/commands/work-on/review.md +241 -29
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +21 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
- package/commands/work-on-monolithic.md +0 -326
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
+
user-invocable: false
|
|
2
3
|
description: Investigate a GitHub issue — validate it's real, determine root cause, post findings
|
|
3
|
-
argument-hint: "
|
|
4
|
+
argument-hint: "{NUMBER} --repo {GH_REPO} --gh-flag \"{GH_FLAG}\""
|
|
5
|
+
context: fork
|
|
4
6
|
---
|
|
5
7
|
<!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
|
|
6
8
|
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
|
|
@@ -11,7 +13,9 @@ argument-hint: "[issue number] [--repo {owner}/{repo}] [--gh-flag \"-R {owner}/{
|
|
|
11
13
|
|
|
12
14
|
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
15
|
|
|
14
|
-
|
|
16
|
+
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).
|
|
17
|
+
|
|
18
|
+
**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
19
|
**NEVER use plan mode (EnterPlanMode).**
|
|
16
20
|
|
|
17
21
|
<!-- FORGE:SPEC_LOADED — work-on/investigate.md loaded and active. Agent is bound by this spec. -->
|
|
@@ -20,15 +24,99 @@ Standalone investigation phase for the work-on pipeline. Validates whether an is
|
|
|
20
24
|
|
|
21
25
|
## Inputs
|
|
22
26
|
|
|
23
|
-
Parse from $ARGUMENTS:
|
|
27
|
+
Parse from $ARGUMENTS (`{NUMBER} --repo {GH_REPO} --gh-flag "{GH_FLAG}"`):
|
|
24
28
|
- `{NUMBER}` — issue number (required)
|
|
25
|
-
- `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`
|
|
29
|
+
- `--repo {GH_REPO}` — GitHub repo (e.g. `{owner}/{repo}`)
|
|
26
30
|
- `--gh-flag {GH_FLAG}` — gh CLI repo flag (e.g. `-R {owner}/{repo}`)
|
|
27
31
|
|
|
28
|
-
|
|
32
|
+
The router passes all three. If `--repo` / `--gh-flag` are absent (standalone use), resolve them from `forge.yaml → project`.
|
|
33
|
+
|
|
34
|
+
**Fail closed**: if `{NUMBER}` is missing/non-numeric, or `{GH_REPO}` cannot be determined, print the result block below and stop — do not guess:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
INVESTIGATE_RESULT:
|
|
38
|
+
status: BLOCKED
|
|
39
|
+
verdict: null
|
|
40
|
+
confidence: null
|
|
41
|
+
decompose: null
|
|
42
|
+
comment_url: null
|
|
43
|
+
gist_url: null
|
|
44
|
+
milestone_index_url: null
|
|
45
|
+
blocker: missing required arg: <which one>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**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
49
|
|
|
30
50
|
---
|
|
31
51
|
|
|
52
|
+
## Script resolution
|
|
53
|
+
|
|
54
|
+
**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`.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Canonical script resolution (keep byte-identical across specs; guarded by scripts/forge-root.test.sh).
|
|
58
|
+
# Shell state does NOT persist between Bash tool calls: include this whole block at the top of
|
|
59
|
+
# every command that uses resolve_script or FORGE_ROOT.
|
|
60
|
+
REPO_PATH="${REPO_PATH:-$(yq '.paths.root // ""' forge.yaml 2>/dev/null)}"
|
|
61
|
+
[ -n "$REPO_PATH" ] && [ "$REPO_PATH" != "null" ] || REPO_PATH="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
62
|
+
ADAPTIVE_DIR_RAW="${REPO_PATH}/$(yq '.adaptive_scripts.directory // ".forgedock/scripts"' forge.yaml 2>/dev/null || echo '.forgedock/scripts')"
|
|
63
|
+
ADAPTIVE_DIR=$(realpath -m "$ADAPTIVE_DIR_RAW" 2>/dev/null || echo "$ADAPTIVE_DIR_RAW")
|
|
64
|
+
ADAPTIVE_ENABLED=$(yq '.adaptive_scripts.enabled // "true"' forge.yaml 2>/dev/null || echo 'true')
|
|
65
|
+
# Bounds check: reject adaptive_scripts.directory values that escape the repo root.
|
|
66
|
+
# Normalize REPO_PATH the same way ADAPTIVE_DIR is normalized (realpath -m) so a trailing
|
|
67
|
+
# slash in paths.root does not inject a '//' into the glob and trigger a false positive.
|
|
68
|
+
REPO_PATH_NORM=$(realpath -m "$REPO_PATH" 2>/dev/null || echo "$REPO_PATH")
|
|
69
|
+
if [[ "$ADAPTIVE_DIR" != "${REPO_PATH_NORM}/"* ]]; then
|
|
70
|
+
echo "WARNING: adaptive_scripts.directory resolves outside repo root ('$ADAPTIVE_DIR') — adaptive tier disabled" >&2
|
|
71
|
+
ADAPTIVE_ENABLED=false
|
|
72
|
+
fi
|
|
73
|
+
# ForgeDock's own install root (holds scripts/ and commands/), resolved ONCE by the canonical bootstrap
|
|
74
|
+
# below — never the consumer repo: plugin installs set neither FORGE_HOME nor FORGEDOCK_HOME, and a
|
|
75
|
+
# repo-relative fallback would execute (or miss) a same-named script controlled by the consumer repo.
|
|
76
|
+
# Resolution: $FORGEDOCK_HOME (authoritative when set) > $FORGE_HOME > $CLAUDE_PLUGIN_ROOT > the
|
|
77
|
+
# ~/.claude/commands symlink target > the Claude Code plugin cache/marketplace dirs. <!-- forge#3098 -->
|
|
78
|
+
# FORGE_ROOT bootstrap (canonical; keep byte-identical across specs, guarded by scripts/forge-root.test.sh)
|
|
79
|
+
FORGE_ROOT=""
|
|
80
|
+
if [ -n "${FORGEDOCK_HOME:-}" ]; then case "$FORGEDOCK_HOME" in /*) FORGE_ROOT="$FORGEDOCK_HOME" ;; esac; else
|
|
81
|
+
# Portable to bash 3.2 (macOS), BSD/GNU coreutils and zsh: no mapfile, no sort -V, no bare globs (zsh aborts on no match).
|
|
82
|
+
_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")")"
|
|
83
|
+
# 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)
|
|
84
|
+
_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-)"
|
|
85
|
+
_m="$(find -L "$HOME/.claude/plugins/marketplaces" -mindepth 1 -maxdepth 1 -type d -iname '*forgedock*' 2>/dev/null)"
|
|
86
|
+
# '${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.
|
|
87
|
+
_k="$(printf '%s\n' '${CLAUDE_PLUGIN_ROOT}' "${FORGE_HOME:-}" "$_l" "$_v" "$_m")"
|
|
88
|
+
while IFS= read -r _c; do
|
|
89
|
+
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
|
|
90
|
+
done <<< "$_k"
|
|
91
|
+
fi
|
|
92
|
+
UNIVERSAL_DIR="${FORGE_ROOT:+$FORGE_ROOT/scripts}" # empty => tier 3 skipped, prose tier
|
|
93
|
+
# NOTE: never resolve this via `which` or `find` — universal scripts are
|
|
94
|
+
# install-relative, not installed on $PATH, so a PATH lookup always misses.
|
|
95
|
+
# FORGE_ROOT (above) is the deterministic resolution; there is NO repo-path fallback.
|
|
96
|
+
# Pipeline agents MUST NOT use `find` (unbounded or filesystem-wide) to
|
|
97
|
+
# locate pipeline scripts under any circumstances: if UNIVERSAL_DIR/${operation}.sh
|
|
98
|
+
# does not exist, resolve_script() falls through to Tier 4 (prose) below,
|
|
99
|
+
# which is always safe and available. A missing script is never a reason
|
|
100
|
+
# to search the filesystem. <!-- Added: forge#1984 -->
|
|
101
|
+
|
|
102
|
+
resolve_script() {
|
|
103
|
+
local operation="$1"
|
|
104
|
+
# Tier 2: per-repo adaptive (skip if disabled)
|
|
105
|
+
if [ "$ADAPTIVE_ENABLED" != "false" ] && [ -f "${ADAPTIVE_DIR}/${operation}.sh" ]; then
|
|
106
|
+
echo "adaptive:${ADAPTIVE_DIR}/${operation}.sh"
|
|
107
|
+
return
|
|
108
|
+
fi
|
|
109
|
+
# Tier 3: universal script
|
|
110
|
+
if [ -n "$UNIVERSAL_DIR" ] && [ -f "${UNIVERSAL_DIR}/${operation}.sh" ]; then
|
|
111
|
+
echo "universal:${UNIVERSAL_DIR}/${operation}.sh"
|
|
112
|
+
return
|
|
113
|
+
fi
|
|
114
|
+
# Tier 4: prose fallback
|
|
115
|
+
echo "prose:"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
|
|
32
120
|
## Phase 0.5: Memory Retrieval — Prior Run Priors <!-- Added: forge#1316 -->
|
|
33
121
|
|
|
34
122
|
**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 +345,124 @@ Store `RECALL_ISSUE_CITATIONS` in a variable — it is used in Phase 1C to popul
|
|
|
257
345
|
|
|
258
346
|
## Phase 1A: Load Issue & Check Resume State
|
|
259
347
|
|
|
348
|
+
Re-read all state from GitHub (this fork has no inherited context):
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
gh issue view {NUMBER} {GH_FLAG} --json number,title,body,labels,state
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Resume check** — run this FIRST (before Phase 0.5):
|
|
355
|
+
|
|
260
356
|
```bash
|
|
261
|
-
gh
|
|
262
|
-
|
|
357
|
+
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)
|
|
358
|
+
[ -z "$COMMENTS_JSON" ] && COMMENTS_JSON='[]'
|
|
359
|
+
|
|
360
|
+
# Complete = INVESTIGATOR header AND (COMPLETE or INVALID sentinel) in the SAME comment. Use the last such comment.
|
|
361
|
+
DONE_JSON=$(echo "$COMMENTS_JSON" | jq -c '[.[] | select(.body | contains("<!-- FORGE:INVESTIGATOR -->"))
|
|
362
|
+
| select((.body | contains("<!-- INVESTIGATION:COMPLETE -->")) or (.body | contains("<!-- INVESTIGATION:INVALID -->")))] | last // empty')
|
|
363
|
+
|
|
364
|
+
# Partial = INVESTIGATOR header but NEITHER sentinel (interrupted run)
|
|
365
|
+
PARTIAL_IDS=$(echo "$COMMENTS_JSON" | jq -r '.[] | select(.body | contains("<!-- FORGE:INVESTIGATOR -->"))
|
|
366
|
+
| select((.body | contains("<!-- INVESTIGATION:COMPLETE -->") | not) and (.body | contains("<!-- INVESTIGATION:INVALID -->") | not)) | .id')
|
|
367
|
+
|
|
368
|
+
if [ -n "$DONE_JSON" ]; then
|
|
369
|
+
DONE_BODY=$(echo "$DONE_JSON" | jq -r '.body')
|
|
370
|
+
DONE_URL=$(echo "$DONE_JSON" | jq -r '.url')
|
|
371
|
+
R_VERDICT=$(printf '%s\n' "$DONE_BODY" | grep -oE '\*\*Verdict\*\*: *[A-Za-z_-]+' | head -1 | sed -E 's/.*: *//')
|
|
372
|
+
R_CONF=$(printf '%s\n' "$DONE_BODY" | grep -oE '\*\*Confidence\*\*: *[A-Za-z]+' | head -1 | sed -E 's/.*: *//')
|
|
373
|
+
if printf '%s\n' "$DONE_BODY" | sed -n '/^### Decomposition Assessment/,/^###[^#]/p' | grep -qE '^\*\*YES\*\*'; then R_DECOMP=YES; else R_DECOMP=NO; fi
|
|
374
|
+
[ "$R_VERDICT" = "INVALID" ] && R_STATUS=INVALID || R_STATUS=ALREADY_DONE
|
|
375
|
+
R_GIST=$(echo "$COMMENTS_JSON" | jq -r '[.[] | .body | select(contains("FORGE:KNOWLEDGE_GIST:"))] | first // ""' | grep -oE 'https://gist[^ ]+' | head -1)
|
|
376
|
+
R_MS_NUM=$(gh issue view {NUMBER} {GH_FLAG} --json milestone --jq '.milestone.number // empty' 2>/dev/null)
|
|
377
|
+
R_MS_IDX=""
|
|
378
|
+
[ -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/ -->$//')
|
|
379
|
+
echo "INVESTIGATE_RESULT:"
|
|
380
|
+
echo " status: $R_STATUS"
|
|
381
|
+
echo " verdict: ${R_VERDICT:-null}"
|
|
382
|
+
echo " confidence: ${R_CONF:-null}"
|
|
383
|
+
echo " decompose: $R_DECOMP"
|
|
384
|
+
echo " comment_url: $DONE_URL"
|
|
385
|
+
echo " gist_url: ${R_GIST:-null}"
|
|
386
|
+
echo " milestone_index_url: ${R_MS_IDX:-null}"
|
|
387
|
+
echo " blocker: null"
|
|
388
|
+
echo "RESUME: investigation already complete — copy the block above verbatim as your final reply and STOP."
|
|
389
|
+
else
|
|
390
|
+
for CID in $PARTIAL_IDS; do
|
|
391
|
+
gh api "repos/{GH_REPO}/issues/comments/${CID}" -X DELETE 2>/dev/null || true # interrupted investigation: delete partial comment and restart
|
|
392
|
+
done
|
|
393
|
+
echo "RESUME: no completed investigation — proceeding with a fresh run."
|
|
394
|
+
fi
|
|
263
395
|
```
|
|
264
396
|
|
|
265
|
-
**Resume logic
|
|
266
|
-
-
|
|
267
|
-
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
397
|
+
**Resume logic** (what the block implements):
|
|
398
|
+
- `<!-- 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`.
|
|
399
|
+
- `<!-- FORGE:INVESTIGATOR -->` comment but NEITHER sentinel → investigation was interrupted; the partial comment(s) are deleted and the run restarts.
|
|
400
|
+
- No investigator comment → fresh investigation.
|
|
401
|
+
|
|
402
|
+
**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:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
406
|
+
case "$TIER" in
|
|
407
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} investigating ;;
|
|
408
|
+
prose)
|
|
409
|
+
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
|
|
410
|
+
;;
|
|
411
|
+
esac
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
### 1A.5: Normalize Issue Body (MANDATORY)
|
|
417
|
+
|
|
418
|
+
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.
|
|
419
|
+
|
|
420
|
+
**Skip if**: All four sections (`## Problem`, `## Affected Files`, `## Expected Behavior`, `## Acceptance Criteria`) are already present.
|
|
272
421
|
|
|
273
|
-
**Set label**:
|
|
274
422
|
```bash
|
|
275
|
-
gh issue
|
|
276
|
-
|
|
423
|
+
ISSUE_BODY=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body')
|
|
424
|
+
|
|
425
|
+
MISSING_SECTIONS=""
|
|
426
|
+
echo "$ISSUE_BODY" | grep -q "^## Problem" || MISSING_SECTIONS="$MISSING_SECTIONS PROBLEM"
|
|
427
|
+
echo "$ISSUE_BODY" | grep -q "^## Affected Files" || MISSING_SECTIONS="$MISSING_SECTIONS AFFECTED_FILES"
|
|
428
|
+
echo "$ISSUE_BODY" | grep -q "^## Expected Behavior" || MISSING_SECTIONS="$MISSING_SECTIONS EXPECTED_BEHAVIOR"
|
|
429
|
+
echo "$ISSUE_BODY" | grep -q "^## Acceptance Criteria" || MISSING_SECTIONS="$MISSING_SECTIONS ACCEPTANCE_CRITERIA"
|
|
430
|
+
|
|
431
|
+
if [ -n "$MISSING_SECTIONS" ]; then
|
|
432
|
+
echo "Missing sections:$MISSING_SECTIONS — normalizing issue body before investigation"
|
|
433
|
+
|
|
434
|
+
APPEND_TEXT=""
|
|
435
|
+
echo "$MISSING_SECTIONS" | grep -q "PROBLEM" && APPEND_TEXT="$APPEND_TEXT
|
|
436
|
+
## Problem
|
|
437
|
+
|
|
438
|
+
Root cause unknown — investigation needed."
|
|
439
|
+
|
|
440
|
+
echo "$MISSING_SECTIONS" | grep -q "AFFECTED_FILES" && APPEND_TEXT="$APPEND_TEXT
|
|
441
|
+
## Affected Files
|
|
442
|
+
|
|
443
|
+
Files to be identified during investigation."
|
|
444
|
+
|
|
445
|
+
echo "$MISSING_SECTIONS" | grep -q "EXPECTED_BEHAVIOR" && APPEND_TEXT="$APPEND_TEXT
|
|
446
|
+
## Expected Behavior
|
|
447
|
+
|
|
448
|
+
Expected behavior to be determined during investigation."
|
|
449
|
+
|
|
450
|
+
echo "$MISSING_SECTIONS" | grep -q "ACCEPTANCE_CRITERIA" && APPEND_TEXT="$APPEND_TEXT
|
|
451
|
+
## Acceptance Criteria
|
|
452
|
+
|
|
453
|
+
- [ ] Fix confirmed during investigation."
|
|
454
|
+
|
|
455
|
+
# Append missing sections to the existing body (never replace — only extend)
|
|
456
|
+
NORMALIZED_BODY="${ISSUE_BODY}${APPEND_TEXT}"
|
|
457
|
+
gh issue edit {NUMBER} {GH_FLAG} --body "$NORMALIZED_BODY" # allowlist:check-command-side-effects
|
|
458
|
+
echo "Issue body normalized — added:$MISSING_SECTIONS"
|
|
459
|
+
else
|
|
460
|
+
echo "Issue body already contains all mandatory sections — skipping normalization"
|
|
461
|
+
fi
|
|
277
462
|
```
|
|
278
463
|
|
|
464
|
+
**Continue to Phase 1B unconditionally.** Normalization is a compensation step — it never blocks investigation.
|
|
465
|
+
|
|
279
466
|
---
|
|
280
467
|
|
|
281
468
|
## Phase 1B: Investigate
|
|
@@ -293,6 +480,14 @@ The target repo is `{GH_REPO}` (resolved from `forge.yaml → project`). The wor
|
|
|
293
480
|
- Infrastructure: `.github/workflows/`, `docker-compose*.yml`, `infra/`
|
|
294
481
|
- Config: `forge.yaml`, `.env.example`, any `config/` directory
|
|
295
482
|
|
|
483
|
+
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.
|
|
484
|
+
|
|
485
|
+
**Workflow pipeline issues** (repo is a ForgeDock installation):
|
|
486
|
+
- Key files: `commands/work-on.md`, `commands/review-pr.md`, `commands/quality-gate.md`, `commands/orchestrate.md`, `forge.yaml`, `bin/forgedock.mjs`
|
|
487
|
+
|
|
488
|
+
**INFRA domain known footguns** (read before writing any `.github/workflows/*.yml` changes):
|
|
489
|
+
- **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`)
|
|
490
|
+
|
|
296
491
|
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
492
|
|
|
298
493
|
### Code Index Query (run BEFORE any grep exploration)
|
|
@@ -322,7 +517,22 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
322
517
|
|
|
323
518
|
1. **Check the right branch** — read from the branch specified in the issue body (`**Code branch**: \`{branch}\``) if present
|
|
324
519
|
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)
|
|
520
|
+
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:
|
|
521
|
+
```bash
|
|
522
|
+
# Check all service layers for the capability (adapt paths to your project structure)
|
|
523
|
+
grep -rn "{capability_keyword}" {REPO_PATH}/services/ --include="*.py" -l | head -20
|
|
524
|
+
# Look for scheduled jobs, automated runners, existing integrations
|
|
525
|
+
grep -rn "scheduler\|celery\|cron\|nightly\|periodic" {REPO_PATH}/services/ --include="*.py" -l | head -10
|
|
526
|
+
```
|
|
527
|
+
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
528
|
3. **Verify claims** — does the code actually have the problem described?
|
|
529
|
+
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:
|
|
530
|
+
```bash
|
|
531
|
+
grep -rn '"field_name"\s*:' services/ # Python dict key assignments
|
|
532
|
+
grep -rn 'result\["field_name"\]\s*=' services/ # Direct assignments
|
|
533
|
+
grep -rn '\.field_name\s*=' services/ # Attribute assignments
|
|
534
|
+
```
|
|
535
|
+
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
536
|
4. **Git blame** — trace when/why the relevant code was written. Run bounded, local commands (no network round-trip):
|
|
327
537
|
```bash
|
|
328
538
|
# Introducing commit for each affected file (first commit that added it)
|
|
@@ -333,9 +543,10 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
333
543
|
git blame -L {start},{end} -- {affected_file}
|
|
334
544
|
```
|
|
335
545
|
Record the introducing commit and last-touch commit for each primary affected file — this feeds the mandatory **History findings** field in Phase 1C.
|
|
546
|
+
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
547
|
5. **Domain context discovery** (narrow scope only, 1–5 files):
|
|
337
548
|
```bash
|
|
338
|
-
git log --oneline --all -30 -- {affected_files} | grep -
|
|
549
|
+
git log --oneline --all -30 -- {affected_files} | grep -oE '#[0-9]+' | sort -u
|
|
339
550
|
gh issue list -R {GH_REPO} --state closed --limit 8 --search "{function_name}"
|
|
340
551
|
```
|
|
341
552
|
Keep only file/function-level overlap. Max 5 related issues. Everything is a hint to verify, not a fact.
|
|
@@ -349,6 +560,17 @@ bash {REPO_PATH}/scripts/code-index.sh query --domain {DOMAIN_LABEL} --repo-path
|
|
|
349
560
|
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
561
|
6. **Determine root cause** — what's actually broken or missing?
|
|
351
562
|
7. **Identify affected files** — full list of files that need changes
|
|
563
|
+
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.
|
|
564
|
+
```bash
|
|
565
|
+
# Identify the broken condition or gated function call from the issue
|
|
566
|
+
# Then search sibling files in the same router/service directory
|
|
567
|
+
AFFECTED_DIR=$(dirname {PRIMARY_AFFECTED_FILE})
|
|
568
|
+
grep -rn "{broken_pattern}" "$AFFECTED_DIR" --include="*.py" | grep -v "{PRIMARY_AFFECTED_FILE}"
|
|
569
|
+
```
|
|
570
|
+
**If identical patterns are found in files NOT listed in the issue spec**, output a scope-gap warning:
|
|
571
|
+
> **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.
|
|
572
|
+
|
|
573
|
+
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
574
|
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
575
|
|
|
354
576
|
---
|
|
@@ -458,7 +680,7 @@ ACCEPTANCE_CHECK: id=ac-4 type=command target="grep -qE '(>= ?2|2\+)' commands/o
|
|
|
458
680
|
|
|
459
681
|
**Check types**:
|
|
460
682
|
- `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)
|
|
683
|
+
- `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
684
|
- `command` — run a shell command and assert exit 0 (`target` = shell command, `matcher` = `exit_0`)
|
|
463
685
|
- `behavior` — assert a runtime/observable behavior via shell command (`target` = shell command, `matcher` = expected output string or regex)
|
|
464
686
|
|
|
@@ -489,12 +711,15 @@ own process. GitHub App installation identities are `Bot` users and cannot use t
|
|
|
489
711
|
|
|
490
712
|
```bash
|
|
491
713
|
if [ -z "${FORGE_GIST_CAPABLE+x}" ]; then
|
|
714
|
+
# Fail closed: only a positively identified user account can use the Gists API. A GitHub App
|
|
715
|
+
# installation token cannot read /user at all (HTTP 403, empty type), so "not Bot" is NOT
|
|
716
|
+
# evidence of capability; an unavailable probe disables Gists rather than letting every
|
|
717
|
+
# worker fail on its first Gist write.
|
|
492
718
|
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.
|
|
719
|
+
if [ "$GIST_AUTH_TYPE" = "User" ]; then
|
|
497
720
|
FORGE_GIST_CAPABLE=true
|
|
721
|
+
else
|
|
722
|
+
FORGE_GIST_CAPABLE=false
|
|
498
723
|
fi
|
|
499
724
|
export FORGE_GIST_CAPABLE
|
|
500
725
|
fi
|
|
@@ -550,10 +775,10 @@ GIST_FILENAME="${REPO_SHORT}_${NUMBER}_${SLUG}.md"
|
|
|
550
775
|
Extract verdict, task type, and confidence from the investigation body, then compose the Gist:
|
|
551
776
|
|
|
552
777
|
```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 -
|
|
778
|
+
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Verdict\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Verdict\*\*: //')
|
|
779
|
+
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Task Type\*\*: .+' | head -1 | sed -E 's/^\*\*Task Type\*\*: //')
|
|
780
|
+
CONFIDENCE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Confidence\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Confidence\*\*: //')
|
|
781
|
+
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Severity\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Severity\*\*: //')
|
|
557
782
|
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
558
783
|
|
|
559
784
|
GIST_CONTENT=$(cat <<GIST_EOF
|
|
@@ -637,16 +862,16 @@ fi
|
|
|
637
862
|
|
|
638
863
|
```bash
|
|
639
864
|
MILESTONE_DESC=$(gh api repos/{GH_REPO}/milestones/${MILESTONE_NUM} --jq '.description // ""')
|
|
640
|
-
EXISTING_INDEX_URL=$(echo "$MILESTONE_DESC" | grep -
|
|
865
|
+
EXISTING_INDEX_URL=$(echo "$MILESTONE_DESC" | grep -oE '<!-- FORGE:MILESTONE_INDEX: https://[^ ]+ -->' | head -1 | sed -E 's/^<!-- FORGE:MILESTONE_INDEX: //; s/ -->$//')
|
|
641
866
|
```
|
|
642
867
|
|
|
643
868
|
### Step 3: Build index entry for this issue
|
|
644
869
|
|
|
645
870
|
```bash
|
|
646
871
|
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 -
|
|
872
|
+
VERDICT=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Verdict\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Verdict\*\*: //')
|
|
873
|
+
SEVERITY=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Severity\*\*: [A-Za-z_-]+' | head -1 | sed -E 's/^\*\*Severity\*\*: //')
|
|
874
|
+
TASK_TYPE=$(echo "$INVESTIGATION_BODY" | grep -oE '\*\*Task Type\*\*: .+' | head -1 | sed -E 's/^\*\*Task Type\*\*: //')
|
|
650
875
|
RECOMMENDATION=$(echo "$INVESTIGATION_BODY" | sed -n '/^### Recommendation/,/^### /p' | head -5 | tail -n +2 | tr '\n' ' ' | cut -c1-120)
|
|
651
876
|
|
|
652
877
|
# GIST_URL comes from Phase 1C.5 (may be empty if Gist creation failed)
|
|
@@ -697,7 +922,7 @@ fi
|
|
|
697
922
|
|
|
698
923
|
```bash
|
|
699
924
|
if [ -n "$EXISTING_INDEX_URL" ]; then
|
|
700
|
-
INDEX_GIST_ID=$(echo "$EXISTING_INDEX_URL" | grep -
|
|
925
|
+
INDEX_GIST_ID=$(echo "$EXISTING_INDEX_URL" | grep -oE '[a-f0-9]{20,}' | tail -1)
|
|
701
926
|
|
|
702
927
|
if [ -z "$INDEX_GIST_ID" ]; then
|
|
703
928
|
echo "WARNING: Could not extract Gist ID from index URL — skipping update"
|
|
@@ -733,7 +958,7 @@ ${INDEX_ENTRY}"
|
|
|
733
958
|
|
|
734
959
|
# Update the Gist
|
|
735
960
|
# gh gist edit does not support stdin via '-'; use a temp file instead
|
|
736
|
-
TMPFILE=$(mktemp
|
|
961
|
+
TMPFILE=$(mktemp "${TMPDIR:-/tmp}/forge-index.XXXXXX")
|
|
737
962
|
echo "$UPDATED_INDEX" > "$TMPFILE"
|
|
738
963
|
gh gist edit "$INDEX_GIST_ID" -f "$INDEX_FILENAME" "$TMPFILE" 2>/dev/null
|
|
739
964
|
EDIT_EXIT=$?
|
|
@@ -822,58 +1047,150 @@ fi
|
|
|
822
1047
|
|
|
823
1048
|
---
|
|
824
1049
|
|
|
825
|
-
|
|
1050
|
+
### 1D.1: Correction Capture (MANDATORY — run after 1D.0, BEFORE the workflow label update) <!-- Added: forge#667 -->
|
|
1051
|
+
|
|
1052
|
+
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.
|
|
1053
|
+
|
|
1054
|
+
**Scan for correction signals**:
|
|
826
1055
|
```bash
|
|
827
|
-
|
|
1056
|
+
# Get repo owner login for filtering.
|
|
1057
|
+
# Tiered resolution — necessary because project.owner is the GitHub org/user NAME,
|
|
1058
|
+
# but comment .user.login is always a personal account login. For org-owned repos
|
|
1059
|
+
# these are structurally different (e.g. org="RapierCraftStudios", commenter="mrdubey"),
|
|
1060
|
+
# so using project.owner directly silently disables correction capture for all org repos.
|
|
1061
|
+
#
|
|
1062
|
+
# Resolution order:
|
|
1063
|
+
# 1. project.owner_login (explicit override — required for org repos where owner ≠ personal login)
|
|
1064
|
+
# 2. gh api repos/{GH_REPO} --jq '.owner.login' (auto-resolves correctly for personal repos)
|
|
1065
|
+
# 3. project.owner (backward-compat fallback — still broken for org repos, but avoids hard failure)
|
|
1066
|
+
REPO_OWNER=$(yq '.project.owner_login // ""' forge.yaml 2>/dev/null || echo '')
|
|
1067
|
+
if [ -z "$REPO_OWNER" ]; then
|
|
1068
|
+
REPO_OWNER=$(gh api repos/{GH_REPO} --jq '.owner.login' 2>/dev/null || echo '')
|
|
1069
|
+
fi
|
|
1070
|
+
if [ -z "$REPO_OWNER" ]; then
|
|
1071
|
+
REPO_OWNER=$(yq '.project.owner' forge.yaml 2>/dev/null || echo '')
|
|
1072
|
+
fi
|
|
1073
|
+
|
|
1074
|
+
# Fetch all comments, filter to owner-only, look for correction signals
|
|
1075
|
+
CORRECTIONS=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1076
|
+
| jq -r --arg owner "$REPO_OWNER" \
|
|
1077
|
+
'.[] | select(.user.login == $owner) | select(
|
|
1078
|
+
(.body | test("no,? use|actually use|use .+ instead|not .+, use|wrong branch"; "i"))
|
|
1079
|
+
) | .body' 2>/dev/null || echo '')
|
|
828
1080
|
```
|
|
829
1081
|
|
|
830
|
-
|
|
1082
|
+
**If correction signals found** — extract and write each correction:
|
|
1083
|
+
|
|
831
1084
|
```bash
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1085
|
+
# Example: extract branch target correction "use develop not staging"
|
|
1086
|
+
# Adjust regex to the correction pattern detected
|
|
1087
|
+
|
|
1088
|
+
if [ -n "$CORRECTIONS" ]; then
|
|
1089
|
+
echo "Correction signals detected — writing to forge.yaml → learned:"
|
|
1090
|
+
echo "$CORRECTIONS"
|
|
1091
|
+
|
|
1092
|
+
# Write to forge.yaml using yq in-place merge (idempotent — yq merge overwrites existing keys)
|
|
1093
|
+
# Always use env variable injection to avoid YAML injection from comment content
|
|
1094
|
+
# Example for branch target correction:
|
|
1095
|
+
# BRANCH_VALUE="develop"
|
|
1096
|
+
# yq eval '.learned.branch_targets.staging = env(BRANCH_VALUE)' -i forge.yaml
|
|
1097
|
+
|
|
1098
|
+
# After writing, emit FORGE:LEARNED annotation
|
|
1099
|
+
LEARNED_KEYS="branch_targets.staging" # replace with actual extracted keys
|
|
1100
|
+
CAPTURED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
1101
|
+
|
|
1102
|
+
# Update captured_at and captured_by metadata
|
|
1103
|
+
CAPTURED_AT_VAL="$CAPTURED_AT" yq eval '.learned.captured_at = env(CAPTURED_AT_VAL)' -i forge.yaml
|
|
1104
|
+
CAPTURED_BY_VAL="work-on/{NUMBER}" yq eval '.learned.captured_by = env(CAPTURED_BY_VAL)' -i forge.yaml
|
|
1105
|
+
|
|
1106
|
+
LEARNED_BODY="<!-- FORGE:LEARNED -->
|
|
1107
|
+
## Learned Pattern Captured
|
|
1108
|
+
|
|
1109
|
+
**Source**: Owner correction in comment on issue #{NUMBER}
|
|
1110
|
+
**Captured at**: $CAPTURED_AT
|
|
1111
|
+
**Keys written**: \`$LEARNED_KEYS\`
|
|
1112
|
+
|
|
1113
|
+
The following project-specific pattern was detected from owner feedback and written to \`forge.yaml → learned:\`. Future sessions will use this override automatically.
|
|
1114
|
+
|
|
1115
|
+
\`\`\`yaml
|
|
1116
|
+
# Written to forge.yaml
|
|
1117
|
+
learned:
|
|
1118
|
+
# {key}: {value}
|
|
1119
|
+
\`\`\`
|
|
1120
|
+
|
|
1121
|
+
**Idempotency**: yq merge-write — re-running will not duplicate entries."
|
|
1122
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "$LEARNED_BODY" # allowlist:check-command-side-effects
|
|
1123
|
+
|
|
1124
|
+
echo "FORGE:LEARNED annotation posted."
|
|
1125
|
+
fi
|
|
837
1126
|
```
|
|
838
1127
|
|
|
839
|
-
|
|
1128
|
+
**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.
|
|
1129
|
+
|
|
1130
|
+
---
|
|
1131
|
+
|
|
1132
|
+
### 1D.2: Update Labels & Return Verdict
|
|
1133
|
+
|
|
1134
|
+
Resolve the posted investigation comment URL once (used in the result block):
|
|
840
1135
|
|
|
841
|
-
**CONFIRMED or PARTIAL with decompose: YES**:
|
|
842
1136
|
```bash
|
|
843
|
-
gh
|
|
1137
|
+
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
1138
|
```
|
|
845
|
-
Do NOT add `workflow:ready-to-build` — the routing loop will invoke `work-on:decompose` based on the `decompose: YES` return value.
|
|
846
1139
|
|
|
847
|
-
|
|
1140
|
+
**CONFIRMED or PARTIAL with decompose: NO** — transition to `ready-to-build` (this phase owns that label):
|
|
848
1141
|
```bash
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1142
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1143
|
+
case "$TIER" in
|
|
1144
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} ready-to-build ;;
|
|
1145
|
+
prose)
|
|
1146
|
+
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
|
|
1147
|
+
;;
|
|
1148
|
+
esac
|
|
854
1149
|
```
|
|
855
1150
|
|
|
856
|
-
|
|
1151
|
+
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
1152
|
|
|
858
|
-
**
|
|
1153
|
+
**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
1154
|
```bash
|
|
860
|
-
gh issue edit {NUMBER} {GH_FLAG} --
|
|
861
|
-
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}"
|
|
1155
|
+
gh issue edit {NUMBER} {GH_FLAG} --remove-label "workflow:investigating" 2>/dev/null || true # allowlist:check-command-side-effects
|
|
862
1156
|
```
|
|
863
|
-
|
|
1157
|
+
|
|
1158
|
+
No `FORGE:CHECKPOINT` is written. Print `INVESTIGATE_RESULT:` with `status: COMPLETE`, `decompose: YES` and STOP.
|
|
1159
|
+
|
|
1160
|
+
**INVALID** — transition to `invalid` and close the issue (terminal):
|
|
1161
|
+
```bash
|
|
1162
|
+
RESOLUTION=$(resolve_script 'transition-label'); TIER="${RESOLUTION%%:*}"; SCRIPT_PATH="${RESOLUTION#*:}"
|
|
1163
|
+
case "$TIER" in
|
|
1164
|
+
adaptive|universal) bash "$SCRIPT_PATH" {NUMBER} {GH_FLAG} invalid ;;
|
|
1165
|
+
prose)
|
|
1166
|
+
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
|
|
1167
|
+
;;
|
|
1168
|
+
esac
|
|
1169
|
+
gh issue close {NUMBER} {GH_FLAG} --comment "Closing as invalid: {reason from investigation}" # allowlist:check-command-side-effects
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
No checkpoint written — INVALID is terminal. Print `INVESTIGATE_RESULT:` with `status: INVALID` and STOP.
|
|
864
1173
|
|
|
865
1174
|
---
|
|
866
1175
|
|
|
867
1176
|
## Output
|
|
868
1177
|
|
|
869
|
-
The subcommand writes its results to GitHub (FORGE:INVESTIGATOR comment).
|
|
1178
|
+
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
1179
|
|
|
871
1180
|
```
|
|
872
1181
|
INVESTIGATE_RESULT:
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1182
|
+
status: {COMPLETE|ALREADY_DONE|INVALID|BLOCKED}
|
|
1183
|
+
verdict: {CONFIRMED|PARTIAL|INVALID|null}
|
|
1184
|
+
confidence: {HIGH|MEDIUM|LOW|null}
|
|
1185
|
+
decompose: {YES|NO|null}
|
|
1186
|
+
comment_url: {url of posted (or pre-existing) comment, or null}
|
|
877
1187
|
gist_url: {url of knowledge gist, or null if creation failed/skipped}
|
|
878
1188
|
milestone_index_url: {url of milestone index gist, or null if no milestone/creation failed}
|
|
1189
|
+
blocker: {one-line reason when status is BLOCKED, else null}
|
|
879
1190
|
```
|
|
1191
|
+
|
|
1192
|
+
**Status mapping**:
|
|
1193
|
+
- `COMPLETE` — fresh investigation finished with verdict CONFIRMED or PARTIAL (`decompose` YES or NO).
|
|
1194
|
+
- `ALREADY_DONE` — resume check found a completed investigation with a non-INVALID verdict; the block carries the parsed existing verdict (Phase 1A).
|
|
1195
|
+
- `INVALID` — verdict INVALID (fresh run, or an existing `INVESTIGATION:INVALID` comment found on resume); issue closed with `workflow:invalid`.
|
|
1196
|
+
- `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.
|