forgedock 1.1.7 → 1.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -33
- package/bin/cinema.mjs +23 -5
- package/bin/engine/invariants.mjs +361 -0
- package/bin/engine-cli.mjs +137 -11
- package/bin/env-detect.mjs +249 -0
- package/bin/forge-utils.mjs +364 -4
- package/bin/forgedock.mjs +1064 -135
- package/bin/hooks/pre-tool-use.mjs +182 -0
- package/bin/hooks/session-start.mjs +68 -3
- package/bin/init-detect.mjs +101 -12
- package/bin/journey.mjs +917 -16
- package/bin/labels.json +10 -0
- package/bin/recall.mjs +557 -0
- package/bin/registry.mjs +91 -3
- package/bin/runner.mjs +38 -2
- package/bin/tui.mjs +49 -3
- package/commands/adopt.md +1 -1
- package/commands/analytics.md +8 -8
- package/commands/audit.md +1 -1
- package/commands/autopilot.md +958 -77
- package/commands/ci-audit.md +1 -1
- package/commands/cleanup.md +139 -55
- package/commands/compat-audit.md +1 -1
- package/commands/deploy-info.md +1 -1
- package/commands/deploy-pr.md +1 -1
- package/commands/diagnose.md +1 -1
- package/commands/explain.md +1 -1
- package/commands/fix-ci.md +1 -1
- package/commands/forgedock-init.md +1 -1
- package/commands/incident-response.md +3 -3
- package/commands/issue.md +1 -1
- package/commands/optimize.md +586 -1
- package/commands/orchestrate/config.md +13 -3
- package/commands/orchestrate/phase-1-resolve.md +36 -26
- package/commands/orchestrate/phase-2-triage.md +1 -1
- package/commands/orchestrate/phase-2.5-synthesis.md +1 -1
- package/commands/orchestrate/phase-3-dependency.md +587 -48
- package/commands/orchestrate/phase-4-execution.md +1031 -57
- package/commands/orchestrate/phase-5-cleanup.md +1 -1
- package/commands/orchestrate/phase-6-report.md +248 -11
- package/commands/orchestrate/safety.md +2 -2
- package/commands/pipeline-health/phase-2-metrics.md +305 -1
- package/commands/pipeline-health/phase-4-proposals.md +127 -0
- package/commands/qa-sweep.md +1 -1
- package/commands/quality-gate.md +418 -1
- package/commands/recover-orphans.md +5 -5
- package/commands/replay.md +1 -1
- package/commands/review-pr-agents/api.md +2 -2
- package/commands/review-pr-agents/auth.md +2 -2
- package/commands/review-pr-agents/billing.md +2 -2
- package/commands/review-pr-agents/concurrency.md +2 -2
- package/commands/review-pr-agents/database.md +2 -2
- package/commands/review-pr-agents/frontend.md +2 -2
- package/commands/review-pr-agents/infra.md +2 -2
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-agents/scraper.md +2 -2
- package/commands/review-pr-agents/security.md +2 -2
- package/commands/review-pr-staging.md +41 -3
- package/commands/review-pr.md +462 -19
- package/commands/rollback.md +1 -1
- package/commands/scope.md +1 -1
- package/commands/security-audit.md +1 -1
- package/commands/spec-doctor.md +429 -0
- package/commands/test-gate.md +1 -1
- package/commands/validate.md +53 -1
- package/commands/work-on/build/architect.md +78 -1
- package/commands/work-on/build/context.md +360 -22
- package/commands/work-on/build/implement.md +3 -1
- package/commands/work-on/build/validate.md +29 -1
- package/commands/work-on/build.md +75 -6
- package/commands/work-on/close.md +624 -10
- package/commands/work-on/decompose.md +1 -1
- package/commands/work-on/investigate.md +235 -5
- package/commands/work-on/remediate.md +331 -0
- package/commands/work-on/review.md +1 -1
- package/commands/work-on-monolithic.md +1 -1
- package/commands/work-on.md +205 -66
- package/docs/CI.md +2 -2
- package/docs/CODEX.md +1 -1
- package/docs/CONFIG.md +186 -0
- package/docs/FORGE-PROTOCOL.md +101 -0
- package/docs/site/command-reference.md +31 -1
- package/docs/site/getting-started.md +9 -34
- package/docs/site/index.md +1 -1
- package/docs/spec/forge-protocol-v1.md +97 -5
- package/docs/spec/label-state-machine.md +6 -1
- package/package.json +3 -2
- package/packages/protocol/src/cli.js +374 -84
- package/packages/protocol/src/types.js +75 -1
- package/scripts/build-knowledge-index.mjs +1761 -0
- package/scripts/calibration.mjs +996 -0
- package/scripts/check-command-docs-drift.sh +4 -2
- package/scripts/danger-zones.mjs +789 -0
- package/scripts/doctor-pipeline-state.sh +88 -2
- package/scripts/select-fix-targets.sh +234 -0
- package/scripts/transition-label.sh +245 -6
- package/scripts/refresh-bot-token.sh +0 -97
|
@@ -12,7 +12,7 @@ argument-hint: [issue number] [--repo GH_REPO] [--gh-flag GH_FLAG] [--pr PR_NUMB
|
|
|
12
12
|
**Invoked by**: `work-on.md` Phase 6–7, after `review.md` returns `REVIEW_RESULT: status: COMPLETE`.
|
|
13
13
|
**Output**: Update project board, close issue, update parent tracker, post trajectory log. Return final summary.
|
|
14
14
|
|
|
15
|
-
**Agent model policy**: `
|
|
15
|
+
**Agent model policy**: `effort: low` (mechanical tier — label transitions, annotation posting, board updates; this file is mechanical end-to-end, so a low effort level is safe here). Fallback: `model: "sonnet"` if rate-limited. Feature gate: pass `effort` only on Claude Code >= 2.1.154. **Note**: this file is dispatched via `Skill("work-on:close", ...)`, which does not support a `model` override — see `work-on.md` section "Model and Effort Tiering — What Actually Applies" for why a `model: "haiku"` claim here would not take effect. <!-- Corrected: forge#1827 -->
|
|
16
16
|
**NEVER use plan mode (EnterPlanMode).**
|
|
17
17
|
|
|
18
18
|
<!-- FORGE:SPEC_LOADED — work-on/close.md loaded and active. Agent is bound by this spec. -->
|
|
@@ -59,6 +59,77 @@ Extract from agent comments:
|
|
|
59
59
|
|
|
60
60
|
---
|
|
61
61
|
|
|
62
|
+
## Phase C0.5: Close-Scope Invariant Assertions (MANDATORY)
|
|
63
|
+
|
|
64
|
+
Run before any close actions. Evaluates `close`-scope invariants declared in
|
|
65
|
+
`forge-invariants.yaml` via `bin/engine/invariants.mjs`. A failed assertion
|
|
66
|
+
logs the violated proposition by name and flags the anomaly in the trajectory
|
|
67
|
+
log — it does NOT abort the close phase (advisory enforcement: flag, then continue).
|
|
68
|
+
|
|
69
|
+
**Skip if**: `forge-invariants.yaml` is absent or `bin/engine/invariants.mjs`
|
|
70
|
+
is unavailable (e.g. fresh install before this file ships). Fail-open.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Read local run-log for this issue (absolute path matches engine run-log dir)
|
|
74
|
+
RUN_LOG_DIR="${HOME}/.forge/runs"
|
|
75
|
+
RUN_LOG_FILE="${RUN_LOG_DIR}/{NUMBER}.jsonl"
|
|
76
|
+
|
|
77
|
+
INVARIANT_ANOMALIES=""
|
|
78
|
+
|
|
79
|
+
if [ -f "${RUN_LOG_FILE}" ] && [ -f "$(dirname "$(which node)")/node" ] 2>/dev/null; then
|
|
80
|
+
# Check close-scope invariants via the evaluator
|
|
81
|
+
INVARIANT_RESULT=$(node -e "
|
|
82
|
+
import(new URL('file://$(pwd)/bin/engine/invariants.mjs'))
|
|
83
|
+
.then(m => {
|
|
84
|
+
const decls = m.loadInvariants('$(pwd)/forge-invariants.yaml');
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
let events = [];
|
|
87
|
+
try {
|
|
88
|
+
const lines = fs.readFileSync('${RUN_LOG_FILE}', 'utf-8').split('\n').filter(Boolean);
|
|
89
|
+
events = lines.flatMap(l => { try { return [JSON.parse(l)]; } catch { return []; } });
|
|
90
|
+
} catch {}
|
|
91
|
+
const results = m.assertCloseInvariants(decls, events);
|
|
92
|
+
const failed = results.filter(r => !r.ok);
|
|
93
|
+
if (failed.length) {
|
|
94
|
+
failed.forEach(r => process.stderr.write(m.formatViolation(r) + '\n'));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.catch(() => process.exit(0)); // fail-open on any error
|
|
99
|
+
" 2>&1) || INVARIANT_ANOMALIES="${INVARIANT_RESULT}"
|
|
100
|
+
|
|
101
|
+
if [ -n "$INVARIANT_ANOMALIES" ]; then
|
|
102
|
+
echo "CLOSE-SCOPE INVARIANT ANOMALY (flagging — close continues):"
|
|
103
|
+
echo "$INVARIANT_ANOMALIES"
|
|
104
|
+
# The anomaly will be recorded in the trajectory log Anomalies field.
|
|
105
|
+
# It does NOT block the close phase.
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Also check: issue must be in CLOSED state after close attempt.
|
|
110
|
+
# This check runs AFTER Phase C2 (ensure issue is closed). Set a sentinel
|
|
111
|
+
# here to be evaluated post-C2:
|
|
112
|
+
CLOSE_INVARIANT_ISSUE_CHECK=true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Post-C2 check** (evaluate after Phase C2 runs the `gh issue close` command):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
if [ "${CLOSE_INVARIANT_ISSUE_CHECK:-false}" = "true" ]; then
|
|
119
|
+
ISSUE_STATE=$(gh issue view {NUMBER} {GH_FLAG} --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
|
|
120
|
+
if [ "$ISSUE_STATE" != "CLOSED" ]; then
|
|
121
|
+
INVARIANT_ANOMALIES="${INVARIANT_ANOMALIES:+$INVARIANT_ANOMALIES; }issue_closed_at_terminal: issue state is ${ISSUE_STATE} (not CLOSED) after close attempt"
|
|
122
|
+
echo "INVARIANT ANOMALY: issue_closed_at_terminal — issue is ${ISSUE_STATE}, not CLOSED"
|
|
123
|
+
# Flag but continue — trajectory Anomalies field will surface this.
|
|
124
|
+
fi
|
|
125
|
+
fi
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `INVARIANT_ANOMALIES` variable is read in Phase C4.5 (trajectory post) and written to the **Anomalies** field.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
|
|
62
133
|
## Phase C1: Final Issue Body Update
|
|
63
134
|
|
|
64
135
|
**Multi-phase guard**: Before checking off items, detect whether the issue has multiple phases. Only check off items belonging to the current completed phase — not all remaining items across future phases.
|
|
@@ -106,6 +177,194 @@ The `REMAINING_AFTER` variable is passed to Phase C2 to decide whether to close.
|
|
|
106
177
|
|
|
107
178
|
---
|
|
108
179
|
|
|
180
|
+
## Phase C1.7: Module Dossier Append (MANDATORY when PR exists) <!-- Added: forge#1733 -->
|
|
181
|
+
|
|
182
|
+
**Goal**: After each merge that touches a module covered by `devdocs/modules/`, append a dated entry so future agents working on that module receive current institutional knowledge through the binding devdocs channel.
|
|
183
|
+
|
|
184
|
+
**This phase is non-blocking** — if the dossier write fails, log the reason and continue to Phase C1.5. Never stall close for dossier maintenance.
|
|
185
|
+
|
|
186
|
+
**Skip if**: `{PR_NUMBER}` is empty (investigation-only tasks) OR `{REPO_PATH}` is unset OR `devdocs/index.yaml` does not contain a `modules:` section OR no PR files match any module glob.
|
|
187
|
+
|
|
188
|
+
### Step 1: Resolve affected files from FORGE:BUILDER comment
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
# Read FORGE:BUILDER comment to get the list of changed files
|
|
192
|
+
BUILDER_COMMENT=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
193
|
+
--jq '[.[] | select(.body | contains("FORGE:BUILDER"))] | last | .body // ""' 2>/dev/null || echo "")
|
|
194
|
+
|
|
195
|
+
# Extract file paths from the Changes section (lines starting with `- \`filepath\``)
|
|
196
|
+
CHANGED_FILES_RAW=$(echo "$BUILDER_COMMENT" \
|
|
197
|
+
| sed -n '/^### Changes/,/^###/p' \
|
|
198
|
+
| grep -oE '`[^`]+`' \
|
|
199
|
+
| tr -d '`' \
|
|
200
|
+
| grep -E '\.' \
|
|
201
|
+
| head -20)
|
|
202
|
+
|
|
203
|
+
# Fallback: try the FORGE:INVESTIGATOR affected files list
|
|
204
|
+
if [ -z "$CHANGED_FILES_RAW" ]; then
|
|
205
|
+
CHANGED_FILES_RAW=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
206
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR"))] | last | .body // ""' 2>/dev/null \
|
|
207
|
+
| sed -n '/### Affected Files/,/###/p' \
|
|
208
|
+
| grep -oE '`[^`]+`' \
|
|
209
|
+
| tr -d '`' \
|
|
210
|
+
| grep -E '\.' \
|
|
211
|
+
| head -20)
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if [ -z "$CHANGED_FILES_RAW" ]; then
|
|
215
|
+
echo "Phase C1.7: No changed files found from FORGE:BUILDER or FORGE:INVESTIGATOR — skipping dossier append"
|
|
216
|
+
# → continue to Phase C1.5
|
|
217
|
+
fi
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Step 2: Match against module globs and append entries
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
CONFIG_FILE="${FORGE_CONFIG:-forge.yaml}"
|
|
224
|
+
DEVDOCS_REL=$(yq '.devdocs.path // "devdocs"' "$CONFIG_FILE" 2>/dev/null || echo "devdocs")
|
|
225
|
+
DEVDOCS_PATH="${REPO_PATH:-{REPO_PATH}}/${DEVDOCS_REL}"
|
|
226
|
+
INDEX_PATH="${DEVDOCS_PATH}/index.yaml"
|
|
227
|
+
|
|
228
|
+
if [ ! -f "$INDEX_PATH" ]; then
|
|
229
|
+
echo "Phase C1.7: ${INDEX_PATH} not found — skipping dossier append"
|
|
230
|
+
# → continue to Phase C1.5
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
# Extract modules entries: "name|glob|path"
|
|
234
|
+
MODULE_ENTRIES=$(yq '.modules[]? | .name + "|" + .glob + "|" + .path' "$INDEX_PATH" 2>/dev/null || echo "")
|
|
235
|
+
|
|
236
|
+
if [ -z "$MODULE_ENTRIES" ]; then
|
|
237
|
+
echo "Phase C1.7: No modules[] section in index.yaml — skipping dossier append"
|
|
238
|
+
# → continue to Phase C1.5
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
DOSSIER_TIMESTAMP=$(date -u +"%Y-%m-%d")
|
|
242
|
+
DOSSIER_UPDATED_MODULES=""
|
|
243
|
+
|
|
244
|
+
# Iterate module entries; for each: check if any changed file matches the glob
|
|
245
|
+
while IFS='|' read -r MOD_NAME MOD_GLOB MOD_PATH; do
|
|
246
|
+
[ -z "$MOD_GLOB" ] || [ -z "$MOD_PATH" ] && continue
|
|
247
|
+
DOSSIER_ABS="${DEVDOCS_PATH}/${MOD_PATH}"
|
|
248
|
+
|
|
249
|
+
MATCHED=0
|
|
250
|
+
# Iterate changed files using while read — not bare for-in (IFS word-split guard per c39758d)
|
|
251
|
+
while IFS= read -r af; do
|
|
252
|
+
[ -z "$af" ] && continue
|
|
253
|
+
AF_BASENAME=$(basename "$af")
|
|
254
|
+
case "$AF_BASENAME" in
|
|
255
|
+
$MOD_GLOB) MATCHED=1; break ;;
|
|
256
|
+
esac
|
|
257
|
+
case "$af" in
|
|
258
|
+
$MOD_GLOB) MATCHED=1; break ;;
|
|
259
|
+
esac
|
|
260
|
+
done <<< "$CHANGED_FILES_RAW"
|
|
261
|
+
|
|
262
|
+
if [ "$MATCHED" -eq 0 ]; then
|
|
263
|
+
continue
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
echo "Phase C1.7: Module '${MOD_NAME}' matched (glob '${MOD_GLOB}') — appending entry to ${MOD_PATH}"
|
|
267
|
+
|
|
268
|
+
# Build entry text
|
|
269
|
+
# One-line summary from the PR title + issue number
|
|
270
|
+
PR_TITLE=$(gh pr view {PR_NUMBER} {GH_FLAG} --json title --jq '.title' 2>/dev/null || echo "untitled")
|
|
271
|
+
DOSSIER_ENTRY="## Entry ${DOSSIER_TIMESTAMP} — ${PR_TITLE} (#{NUMBER})
|
|
272
|
+
|
|
273
|
+
PR #{PR_NUMBER} touched \`${MOD_NAME}\`. See FORGE:BUILDER comment on issue #{NUMBER} for full change list.
|
|
274
|
+
Key gotcha recorded: (update this entry by editing \`${MOD_PATH}\` in a follow-up PR if the change revealed a new failure mode).
|
|
275
|
+
Cite: #${NUMBER} / PR #{PR_NUMBER}."
|
|
276
|
+
|
|
277
|
+
# Ensure dossier file exists (create skeleton if missing — allows operator to create
|
|
278
|
+
# a new module entry in index.yaml before the dossier file is seeded)
|
|
279
|
+
if [ ! -f "$DOSSIER_ABS" ]; then
|
|
280
|
+
mkdir -p "$(dirname "$DOSSIER_ABS")"
|
|
281
|
+
cat > "$DOSSIER_ABS" <<DOSSIER_INIT_EOF
|
|
282
|
+
---
|
|
283
|
+
module: ${MOD_NAME}
|
|
284
|
+
glob: "${MOD_GLOB}"
|
|
285
|
+
authority: required
|
|
286
|
+
token_cost: 200
|
|
287
|
+
last_compacted: "${DOSSIER_TIMESTAMP}"
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
# Module Dossier: ${MOD_NAME}
|
|
291
|
+
|
|
292
|
+
Rolling per-module knowledge log. Each entry is 3–5 lines with a citation.
|
|
293
|
+
Hard cap: 150 lines. Entries are appended by close.md Phase C1.7 after each
|
|
294
|
+
PR that touches this module. When the file exceeds 150 lines, oldest entries
|
|
295
|
+
are compacted into the Summary block (LLM compaction, in-run).
|
|
296
|
+
|
|
297
|
+
## Summary
|
|
298
|
+
|
|
299
|
+
_No compacted history yet. Dossier was auto-created on ${DOSSIER_TIMESTAMP} by close.md Phase C1.7._
|
|
300
|
+
DOSSIER_INIT_EOF
|
|
301
|
+
echo "Phase C1.7: Created new dossier skeleton at ${DOSSIER_ABS}"
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# Append entry (avoid subshell — use file redirect directly)
|
|
305
|
+
printf '\n%s\n' "$DOSSIER_ENTRY" >> "$DOSSIER_ABS"
|
|
306
|
+
|
|
307
|
+
# Compact if over 150 lines
|
|
308
|
+
DOSSIER_LINE_COUNT=$(wc -l < "$DOSSIER_ABS" 2>/dev/null || echo 0)
|
|
309
|
+
if [ "$DOSSIER_LINE_COUNT" -gt 150 ]; then
|
|
310
|
+
echo "Phase C1.7: Dossier ${MOD_PATH} has ${DOSSIER_LINE_COUNT} lines (>150) — compacting oldest entries"
|
|
311
|
+
# LLM compaction: read the dossier, summarize oldest Entry blocks into the ## Summary
|
|
312
|
+
# section, keeping the most recent 3 entries intact.
|
|
313
|
+
# Implementation note: this is prose-instruction compaction (LLM reads the file and
|
|
314
|
+
# rewrites it). The compacted file must preserve the frontmatter and ## Summary block;
|
|
315
|
+
# it may replace old ## Entry blocks with a single "## Archived Summary (compacted)"
|
|
316
|
+
# block. After compaction, update frontmatter last_compacted to today's date.
|
|
317
|
+
# The compacted file MUST be ≤ 150 lines. If compaction fails (e.g. LLM context
|
|
318
|
+
# overflow), log a warning and leave the file as-is — never delete entries silently.
|
|
319
|
+
echo "COMPACT INSTRUCTION: Read ${DOSSIER_ABS}. Keep the frontmatter (lines between ---), keep the ## Summary section, keep the 3 most recent ## Entry blocks, and replace all older ## Entry blocks with a single '## Archived Summary (compacted — ${DOSSIER_TIMESTAMP})' block containing a 5–8 line distillation of the key failure modes, gotchas, and citations from the archived entries. Write the result back to ${DOSSIER_ABS}. The output MUST be ≤ 150 lines. Update frontmatter last_compacted to ${DOSSIER_TIMESTAMP}."
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
DOSSIER_UPDATED_MODULES="${DOSSIER_UPDATED_MODULES} ${MOD_NAME}"
|
|
323
|
+
|
|
324
|
+
done <<< "$MODULE_ENTRIES"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Step 3: Commit dossier changes and post annotation
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
if [ -n "$DOSSIER_UPDATED_MODULES" ]; then
|
|
331
|
+
# Commit the updated dossier files
|
|
332
|
+
cd "{REPO_PATH}"
|
|
333
|
+
CHANGED_DOSSIER_FILES=$(echo "$DOSSIER_UPDATED_MODULES" | tr ' ' '\n' | while IFS= read -r mod; do
|
|
334
|
+
yq ".modules[]? | select(.name == \"${mod}\") | \"${DEVDOCS_REL}/\" + .path" "$INDEX_PATH" 2>/dev/null
|
|
335
|
+
done | grep -v '^$')
|
|
336
|
+
|
|
337
|
+
if [ -n "$CHANGED_DOSSIER_FILES" ]; then
|
|
338
|
+
# CHANGED_DOSSIER_FILES is newline-separated — iterate so paths containing
|
|
339
|
+
# spaces are staged individually rather than word-split by the shell.
|
|
340
|
+
while IFS= read -r dossier_file; do
|
|
341
|
+
[ -n "$dossier_file" ] || continue
|
|
342
|
+
git -C "{REPO_PATH}" add "$dossier_file" 2>/dev/null || true
|
|
343
|
+
done <<< "$CHANGED_DOSSIER_FILES"
|
|
344
|
+
# Only commit if there are staged changes (new or modified dossier files)
|
|
345
|
+
if ! git -C "{REPO_PATH}" diff --cached --quiet 2>/dev/null; then
|
|
346
|
+
git -C "{REPO_PATH}" commit -s -m "docs(dossier): append entry for PR #{PR_NUMBER} (#${NUMBER})" 2>/dev/null || true
|
|
347
|
+
echo "Phase C1.7: Dossier commit created for modules:${DOSSIER_UPDATED_MODULES}"
|
|
348
|
+
else
|
|
349
|
+
echo "Phase C1.7: No staged dossier changes — skipping commit"
|
|
350
|
+
fi
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# Post annotation on the issue
|
|
354
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:DOSSIER_UPDATED -->
|
|
355
|
+
Module dossier(s) updated:${DOSSIER_UPDATED_MODULES}
|
|
356
|
+
|
|
357
|
+
Entries appended to \`devdocs/modules/\` after PR #{PR_NUMBER} merged. Future agents working
|
|
358
|
+
on these modules will receive the updated knowledge through the devdocs channel (context.md Phase C-1).
|
|
359
|
+
|
|
360
|
+
<!-- FORGE:DOSSIER_UPDATED:COMPLETE -->" 2>/dev/null || true
|
|
361
|
+
else
|
|
362
|
+
echo "Phase C1.7: No module dossiers matched changed files — skipping"
|
|
363
|
+
fi
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
109
368
|
## Phase C1.5: Project Board Update (Status=Done, Workflow=Merged)
|
|
110
369
|
|
|
111
370
|
Update the project board to reflect the merged state. This replaces the old Phase 5E project board update that existed before the modular refactor.
|
|
@@ -217,9 +476,10 @@ gh issue edit {NUMBER} {GH_FLAG} \
|
|
|
217
476
|
|
|
218
477
|
**Skip if**: Issue body does NOT contain a parent issue reference (e.g. `Part of #NNN`) or the issue has no parent in its milestone tracker.
|
|
219
478
|
|
|
220
|
-
Detect parent reference:
|
|
479
|
+
Detect parent reference. Markdown emphasis markers (`**bold**`, `__bold__`, `*italic*`) are stripped before matching, since sub-issue bodies commonly render the label as `**Parent**: #NNN` and the bare label alternation below would otherwise fail to match past the emphasis characters:
|
|
221
480
|
```bash
|
|
222
481
|
PARENT_REF=$(gh issue view {NUMBER} {GH_FLAG} --json body --jq '.body' \
|
|
482
|
+
| sed -E 's/[*_]+//g' \
|
|
223
483
|
| grep -oP '(?i)(part of|spawned from|sub-issue of|parent issue[:]?|parent[:])\s*#\K\d+' \
|
|
224
484
|
| head -1)
|
|
225
485
|
```
|
|
@@ -404,7 +664,58 @@ CARD_JSON=$(jq -nc \
|
|
|
404
664
|
|
|
405
665
|
## Phase C5: Trajectory Log (MANDATORY)
|
|
406
666
|
|
|
407
|
-
Post the `<!-- FORGE:TRAJECTORY -->` comment
|
|
667
|
+
**CODEC PATH (forge#1727)**: Post the `<!-- FORGE:TRAJECTORY -->` comment via the protocol codec — do NOT hand-roll the opening tag. Use `forge-annotation.sh write TRAJECTORY --field ...` or `node packages/protocol/src/cli.js emit TRAJECTORY` to produce the opening tag. The codec handles any field escaping automatically.
|
|
668
|
+
|
|
669
|
+
```bash
|
|
670
|
+
# Codec produces the opening <!-- FORGE:TRAJECTORY --> tag
|
|
671
|
+
TRAJECTORY_HEADER=$(node packages/protocol/src/cli.js emit TRAJECTORY)
|
|
672
|
+
# $TRAJECTORY_HEADER = "<!-- FORGE:TRAJECTORY -->"
|
|
673
|
+
# Append the Markdown body sections, then post via gh issue comment.
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Post the `<!-- FORGE:TRAJECTORY -->` comment as the final pipeline record.
|
|
677
|
+
|
|
678
|
+
**Prior delta computation** — read cost-prior for this issue's task_type × module before posting (forge#1743):
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
# Compute actual vs prior cost delta for self-correction of cost priors
|
|
682
|
+
COST_PRIORS_PATH="${HOME}/.forge/index/cost-priors.json"
|
|
683
|
+
ACTUAL_TOTAL_USD=""
|
|
684
|
+
PRIOR_EST_USD=""
|
|
685
|
+
COST_DELTA_NOTE=""
|
|
686
|
+
|
|
687
|
+
# Read actual spend from FORGE:BUILDER / FORGE:TRAJECTORY best-effort telemetry
|
|
688
|
+
# (same extraction used in work-on.md Phase 7C DECISION_RECORD cost block)
|
|
689
|
+
ACTUAL_TOTAL_USD=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
690
|
+
--jq '[.[] | select(.body | contains("FORGE:BUILDER")) | .body] | last // ""' 2>/dev/null \
|
|
691
|
+
| grep -oP '(?<=cost_usd: )\S+' | head -1 || echo "")
|
|
692
|
+
|
|
693
|
+
if [ -n "$ACTUAL_TOTAL_USD" ] && [ -f "$COST_PRIORS_PATH" ]; then
|
|
694
|
+
# Derive task_type:module key (same logic as Step 3E.5)
|
|
695
|
+
TASK_TYPE=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
696
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
|
|
697
|
+
| grep -oP '(?<=\*\*Task Type\*\*: )\S+' | head -1 | tr '[:upper:]' '[:lower:]' | tr ' ' '-' || echo 'unknown')
|
|
698
|
+
PRIMARY_FILE=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
699
|
+
--jq '[.[] | select(.body | contains("FORGE:INVESTIGATOR")) | .body] | last // ""' 2>/dev/null \
|
|
700
|
+
| grep -oP '`[^`]+\.(py|mjs|ts|md|sh|yaml|yml)`' | tr -d '`' | head -1 || echo '')
|
|
701
|
+
MODULE=$(basename "${PRIMARY_FILE:-_unknown}" | sed 's/\.[^.]*$//' | tr '[:upper:]' '[:lower:]')
|
|
702
|
+
[ -z "$MODULE" ] && MODULE="_unknown"
|
|
703
|
+
PRIOR_KEY="${TASK_TYPE}:${MODULE}"
|
|
704
|
+
|
|
705
|
+
PRIOR_EST_USD=$(jq -r --arg k "$PRIOR_KEY" '.priors[$k].mean // empty' "$COST_PRIORS_PATH" 2>/dev/null || echo '')
|
|
706
|
+
|
|
707
|
+
if [ -n "$PRIOR_EST_USD" ]; then
|
|
708
|
+
DELTA=$(echo "scale=4; $ACTUAL_TOTAL_USD - $PRIOR_EST_USD" | bc 2>/dev/null || echo "?")
|
|
709
|
+
COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=\$${PRIOR_EST_USD} delta=${DELTA} key=${PRIOR_KEY}"
|
|
710
|
+
else
|
|
711
|
+
COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=absent (no prior for key ${PRIOR_KEY})"
|
|
712
|
+
fi
|
|
713
|
+
elif [ -n "$ACTUAL_TOTAL_USD" ]; then
|
|
714
|
+
COST_DELTA_NOTE="actual=\$${ACTUAL_TOTAL_USD} prior=index-absent"
|
|
715
|
+
else
|
|
716
|
+
COST_DELTA_NOTE="no-telemetry"
|
|
717
|
+
fi
|
|
718
|
+
```
|
|
408
719
|
|
|
409
720
|
```bash
|
|
410
721
|
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
|
|
@@ -426,19 +737,39 @@ gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:TRAJECTORY -->
|
|
|
426
737
|
- Decomposition skipped: {DECOMPOSE_REASON}
|
|
427
738
|
- PR merged to: \`{PR_BASE}\` (feature lane, milestone branch)
|
|
428
739
|
|
|
740
|
+
**Cost (economic scheduling)**: ${COST_DELTA_NOTE}
|
|
741
|
+
|
|
429
742
|
**Anomalies**: None
|
|
430
743
|
|
|
431
744
|
**Pipeline completed**: {TIMESTAMP}
|
|
432
745
|
|
|
433
|
-
|
|
746
|
+
$(node packages/protocol/src/cli.js emit CARD --b64 \
|
|
747
|
+
--field issue={NUMBER} \
|
|
748
|
+
--field status={CARD_STATUS} \
|
|
749
|
+
--field pipeline="${PIPELINE_LINE}" \
|
|
750
|
+
--field pr={PR_NUMBER} \
|
|
751
|
+
--field pr_target="${PR_TARGET}" \
|
|
752
|
+
--field commits="${COMMITS}" \
|
|
753
|
+
--field additions="${ADDITIONS}" \
|
|
754
|
+
--field deletions="${DELETIONS}" \
|
|
755
|
+
--field review="${REVIEW_SUMMARY}" \
|
|
756
|
+
--field elapsed="${ELAPSED_SECS:-0}")"
|
|
434
757
|
```
|
|
435
758
|
|
|
436
|
-
The `<!-- FORGE:CARD
|
|
437
|
-
Phase C4.5c
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
759
|
+
The `<!-- FORGE:CARD: v1 sha:... b64:... -->` line carries the machine-readable summary computed in
|
|
760
|
+
Phase C4.5c, encoded as Base64url (design decision 2026-07-08: encoding beats escaping — the
|
|
761
|
+
Base64url alphabet cannot contain HTML comment delimiters by construction). It is wrapped in
|
|
762
|
+
the inline-value annotation form `<!-- FORGE:CARD: ... -->` so parse() extracts the encoded
|
|
763
|
+
payload. Platform consumers (e.g., `/orchestrate`) decode via `node packages/protocol/src/cli.js
|
|
764
|
+
parse --type CARD --field status`. This block is **additive**: all existing `FORGE:TRAJECTORY`
|
|
765
|
+
consumers select via `contains("FORGE:TRAJECTORY")` and parse the markdown table, so the
|
|
766
|
+
embedded CARD line does not affect them.
|
|
767
|
+
|
|
768
|
+
**CODEC PATH (forge#1727)**: The `$(node packages/protocol/src/cli.js emit CARD --b64 ...)` call
|
|
769
|
+
above replaces the previous `<!-- FORGE:CARD ${CARD_JSON} -->` inline-JSON form. The Base64url
|
|
770
|
+
form is safe against all HTML comment injection vectors and includes a sha8 integrity prefix for
|
|
771
|
+
truncation detection. Consumers that parsed the old inline-JSON form must migrate to the codec
|
|
772
|
+
parse path: `echo '...' | node packages/protocol/src/cli.js parse --type CARD --field <key>`.
|
|
442
773
|
|
|
443
774
|
Where:
|
|
444
775
|
- `{PARENT_STATUS}` = `⏭ Skipped` (if no parent) or `✅ Complete` (if parent updated)
|
|
@@ -448,6 +779,29 @@ Where:
|
|
|
448
779
|
|
|
449
780
|
---
|
|
450
781
|
|
|
782
|
+
## Phase C5.1: Knowledge Index + Cost Prior Update (forge#1743) <!-- Added: forge#1743 -->
|
|
783
|
+
|
|
784
|
+
**Goal**: Re-index this issue's knowledge cards and regenerate cost-priors.json so that economic scheduling (orchestrate Step 3E.5) has up-to-date data for future runs. The actual-vs-prior delta recorded in the TRAJECTORY above is the write side of the self-correction loop — this step performs the read/recompute.
|
|
785
|
+
|
|
786
|
+
**This phase is non-blocking** — if the indexer fails, log the reason and continue to Phase C5.2. Never stall close for the cost-prior update.
|
|
787
|
+
|
|
788
|
+
**Skip if**: Terminal state is `INVALID` (no useful cost data from invalid issues).
|
|
789
|
+
|
|
790
|
+
```bash
|
|
791
|
+
# Re-index this issue and regenerate cost priors — non-blocking
|
|
792
|
+
INDEXER_PATH=$(dirname "$(realpath "$0" 2>/dev/null || echo '.')")/../../scripts/build-knowledge-index.mjs
|
|
793
|
+
if node --version >/dev/null 2>&1 && [ -f "$INDEXER_PATH" ]; then
|
|
794
|
+
echo "[cost-prior] Re-indexing issue #${NUMBER} and regenerating cost priors..."
|
|
795
|
+
node "$INDEXER_PATH" --issue {NUMBER} --no-mirror 2>&1 | tail -5 \
|
|
796
|
+
&& echo "[cost-prior] Cost priors updated" \
|
|
797
|
+
|| echo "WARNING: Cost prior update failed — continuing (non-blocking)"
|
|
798
|
+
else
|
|
799
|
+
echo "[cost-prior] Indexer not available — skipping cost prior update (non-blocking)"
|
|
800
|
+
fi
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
451
805
|
## Phase C5.2: Memory Index Update <!-- Added: forge#1316 -->
|
|
452
806
|
|
|
453
807
|
**Goal**: Append this run's learnings to the per-repo memory index so future `investigate` runs can retrieve relevant priors. This is the write side of the compounding intelligence loop.
|
|
@@ -571,6 +925,266 @@ fi
|
|
|
571
925
|
|
|
572
926
|
---
|
|
573
927
|
|
|
928
|
+
## Phase C5.3: Knowledge Ledger Index <!-- Added: forge#1732 -->
|
|
929
|
+
|
|
930
|
+
**Goal**: Index the just-closed issue into the Forge Ledger so future context phases can retrieve
|
|
931
|
+
its knowledge cards by file path or symbol without making live GitHub API calls.
|
|
932
|
+
|
|
933
|
+
**This phase is non-blocking** — if the indexer fails, log the reason and continue to Phase C5.5.
|
|
934
|
+
Never stall close for ledger indexing.
|
|
935
|
+
|
|
936
|
+
**Skip if**: Terminal state is `INVALID` (no confirmed findings to index) OR a `<!-- FORGE:LEDGER_INDEXED -->` comment already exists on the issue (idempotency guard).
|
|
937
|
+
|
|
938
|
+
**Requires**: `scripts/build-knowledge-index.mjs` present in the repository root. If absent, skip
|
|
939
|
+
with a warning — the feature may not be installed on this version.
|
|
940
|
+
|
|
941
|
+
### Step 1: Idempotency check
|
|
942
|
+
|
|
943
|
+
```bash
|
|
944
|
+
LEDGER_INDEXED=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
945
|
+
--jq '[.[] | select(.body | contains("FORGE:LEDGER_INDEXED"))] | length > 0' 2>/dev/null || echo "false")
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**Skip to Phase C5.5 if `$LEDGER_INDEXED == "true"`**.
|
|
949
|
+
|
|
950
|
+
### Step 2: Run incremental indexer
|
|
951
|
+
|
|
952
|
+
Resolve the indexer script path relative to the repository root:
|
|
953
|
+
|
|
954
|
+
```bash
|
|
955
|
+
INDEXER_PATH="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/scripts/build-knowledge-index.mjs"
|
|
956
|
+
|
|
957
|
+
if [ ! -f "$INDEXER_PATH" ]; then
|
|
958
|
+
echo "[LEDGER] scripts/build-knowledge-index.mjs not found — skipping Phase C5.3"
|
|
959
|
+
echo "[LEDGER] Install: update ForgeDock to a version that ships this file"
|
|
960
|
+
else
|
|
961
|
+
# Run incremental indexer for this issue only — ~2 API calls
|
|
962
|
+
LEDGER_EXIT=0
|
|
963
|
+
LEDGER_OUTPUT=$(node "$INDEXER_PATH" \
|
|
964
|
+
--issue {NUMBER} \
|
|
965
|
+
--repo {GH_REPO} \
|
|
966
|
+
--no-mirror \
|
|
967
|
+
2>&1) || LEDGER_EXIT=$?
|
|
968
|
+
|
|
969
|
+
if [ $LEDGER_EXIT -eq 0 ]; then
|
|
970
|
+
echo "[LEDGER] Issue #{NUMBER} indexed into Forge Ledger"
|
|
971
|
+
|
|
972
|
+
# Mirror update: run separately after single-issue index so mirror has up-to-date postings
|
|
973
|
+
node "$INDEXER_PATH" --issue {NUMBER} --repo {GH_REPO} \
|
|
974
|
+
2>/dev/null || true # Non-blocking: mirror failure does not affect local index
|
|
975
|
+
|
|
976
|
+
# Post audit annotation (idempotency guard for future close runs)
|
|
977
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:LEDGER_INDEXED -->
|
|
978
|
+
Issue #{NUMBER} has been indexed into the Forge Ledger.
|
|
979
|
+
|
|
980
|
+
Knowledge cards extracted from FORGE:INVESTIGATOR and FORGE:TRAJECTORY annotations are now
|
|
981
|
+
queryable via \`forge recall\` (exact file/symbol lookup or free-text BM25 search).
|
|
982
|
+
|
|
983
|
+
\`\`\`
|
|
984
|
+
forge recall --file {PRIMARY_AFFECTED_FILE} --json
|
|
985
|
+
\`\`\`
|
|
986
|
+
|
|
987
|
+
<!-- FORGE:LEDGER_INDEXED:COMPLETE -->" 2>/dev/null || true
|
|
988
|
+
|
|
989
|
+
else
|
|
990
|
+
echo "[LEDGER] WARNING: Indexer exited with code ${LEDGER_EXIT} — continuing"
|
|
991
|
+
echo "[LEDGER] Output: ${LEDGER_OUTPUT}"
|
|
992
|
+
fi
|
|
993
|
+
fi
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Watermark semantics
|
|
997
|
+
|
|
998
|
+
The indexer's watermark is `max(issue.updated_at)` across all indexed issues. Indexing a
|
|
999
|
+
single issue via `--issue` advances the watermark only if this issue's `updated_at` is newer
|
|
1000
|
+
than the stored watermark — ensuring the next full incremental run starts from the right
|
|
1001
|
+
position and does not re-scan already-indexed history.
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## Phase C5.4: Auto-ADR Extraction from TRAJECTORY Decisions <!-- Added: forge#1737 -->
|
|
1006
|
+
|
|
1007
|
+
**Goal**: Promote tradeoff-shaped Decisions bullets from the FORGE:TRAJECTORY comment into
|
|
1008
|
+
human-readable, git-tracked ADR markdown files at `devdocs/decisions/NNN-{slug}.md`. Architect
|
|
1009
|
+
plans on future runs will load matching ADRs as constraints before writing any code.
|
|
1010
|
+
|
|
1011
|
+
**This phase is non-blocking** — if ADR extraction, file write, or commit fails at any step,
|
|
1012
|
+
log the reason and continue to Phase C5.5. Never stall close for ADR generation.
|
|
1013
|
+
|
|
1014
|
+
**Skip if**: Terminal state is `INVALID` (no useful decisions to record) OR a
|
|
1015
|
+
`<!-- FORGE:ADR_EXTRACTED -->` comment already exists on the issue (idempotency guard) OR
|
|
1016
|
+
`devdocs/decisions/` directory does not exist in the repository root (feature not installed).
|
|
1017
|
+
|
|
1018
|
+
### What is a "tradeoff-shaped" decision?
|
|
1019
|
+
|
|
1020
|
+
A Decisions bullet is extractable if it contains BOTH:
|
|
1021
|
+
1. A **choice indicator**: words like `chose`, `use`, `prefer`, `process substitution`, `over`,
|
|
1022
|
+
`instead`, `rather than`, `not X`
|
|
1023
|
+
2. A **rationale connector**: `because`, `since`, `so`, `to avoid`, `prevents`, `due to`
|
|
1024
|
+
|
|
1025
|
+
Generic bullets like `- Decomposition skipped: single-concern change` do NOT match and are ignored.
|
|
1026
|
+
|
|
1027
|
+
### Step 1: Idempotency check
|
|
1028
|
+
|
|
1029
|
+
```bash
|
|
1030
|
+
ADR_EXTRACTED=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1031
|
+
--jq '[.[] | select(.body | contains("FORGE:ADR_EXTRACTED"))] | length > 0' 2>/dev/null || echo "false")
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
**Skip to Phase C5.5 if `$ADR_EXTRACTED == "true"`**.
|
|
1035
|
+
|
|
1036
|
+
### Step 2: Extract TRAJECTORY Decisions
|
|
1037
|
+
|
|
1038
|
+
```bash
|
|
1039
|
+
# Read the TRAJECTORY comment posted in Phase C5
|
|
1040
|
+
TRAJECTORY_BODY=$(gh api repos/{GH_REPO}/issues/{NUMBER}/comments \
|
|
1041
|
+
--jq '[.[] | select(.body | contains("FORGE:TRAJECTORY"))] | last | .body // ""' 2>/dev/null || echo '')
|
|
1042
|
+
|
|
1043
|
+
# Extract the Decisions section (lines between **Decisions**: and **Anomalies**:)
|
|
1044
|
+
DECISIONS_RAW=$(echo "$TRAJECTORY_BODY" \
|
|
1045
|
+
| awk '/^\*\*Decisions\*\*:/{found=1; next} /^\*\*Anomalies\*\*:/{found=0} found{print}' \
|
|
1046
|
+
| grep -v '^\s*$' \
|
|
1047
|
+
| head -20) # Cap at 20 bullets to prevent runaway parsing
|
|
1048
|
+
|
|
1049
|
+
if [ -z "$DECISIONS_RAW" ] || echo "$DECISIONS_RAW" | grep -qi "^- None$\|^None$"; then
|
|
1050
|
+
echo "[ADR] No Decisions section found in TRAJECTORY — skipping ADR extraction"
|
|
1051
|
+
ADR_FILES_WRITTEN=0
|
|
1052
|
+
fi
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
### Step 3: Parse and filter for tradeoff shape
|
|
1056
|
+
|
|
1057
|
+
```bash
|
|
1058
|
+
ADR_FILES_WRITTEN=0
|
|
1059
|
+
DECISIONS_DIR="${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}/devdocs/decisions"
|
|
1060
|
+
|
|
1061
|
+
if [ -z "$DECISIONS_RAW" ] || ! [ -d "$DECISIONS_DIR" ]; then
|
|
1062
|
+
echo "[ADR] Skipping: no decisions or devdocs/decisions/ absent"
|
|
1063
|
+
else
|
|
1064
|
+
# Get commit SHA for anchor citation
|
|
1065
|
+
COMMIT_SHA=$(git -C "${REPO_PATH:-$(git rev-parse --show-toplevel 2>/dev/null)}" \
|
|
1066
|
+
rev-parse HEAD 2>/dev/null | head -c 12 || echo "unknown")
|
|
1067
|
+
|
|
1068
|
+
while IFS= read -r bullet; do
|
|
1069
|
+
# Strip leading "- " or "* "
|
|
1070
|
+
text=$(echo "$bullet" | sed 's/^[-*]\s*//')
|
|
1071
|
+
[ -z "$text" ] && continue
|
|
1072
|
+
|
|
1073
|
+
# Tradeoff shape filter: must have a choice indicator + rationale connector
|
|
1074
|
+
CHOICE_MATCH=$(echo "$text" | grep -iE 'chose|use |prefer|instead|rather than|over |not [a-z]|process substitution|branched from|avoided' || true)
|
|
1075
|
+
RATIONALE_MATCH=$(echo "$text" | grep -iE 'because|since[[:space:]]|so[[:space:]]|to avoid|prevents|due to' || true)
|
|
1076
|
+
|
|
1077
|
+
if [ -z "$CHOICE_MATCH" ] || [ -z "$RATIONALE_MATCH" ]; then
|
|
1078
|
+
echo "[ADR] Skipped (not a tradeoff): $text"
|
|
1079
|
+
continue
|
|
1080
|
+
fi
|
|
1081
|
+
|
|
1082
|
+
# Extract anchor: first backtick-quoted path-like string in the decision text
|
|
1083
|
+
ANCHOR_PATH=$(echo "$text" | grep -oE '`[a-zA-Z][^`]*/[^`]+`' | head -1 | tr -d '`' || true)
|
|
1084
|
+
|
|
1085
|
+
# Build slug from first 6 words of decision (lowercase, hyphenated)
|
|
1086
|
+
SLUG=$(echo "$text" | tr '[:upper:]' '[:lower:]' | \
|
|
1087
|
+
sed 's/[^a-z0-9 ]/ /g' | tr -s ' ' '-' | \
|
|
1088
|
+
cut -c1-40 | sed 's/-$//')
|
|
1089
|
+
ADR_FILENAME="${NUMBER}-${SLUG}.md"
|
|
1090
|
+
ADR_PATH="$DECISIONS_DIR/$ADR_FILENAME"
|
|
1091
|
+
|
|
1092
|
+
# Idempotency: skip if file already exists
|
|
1093
|
+
if [ -f "$ADR_PATH" ]; then
|
|
1094
|
+
echo "[ADR] Already exists — skipping: $ADR_FILENAME"
|
|
1095
|
+
ADR_FILES_WRITTEN=$((ADR_FILES_WRITTEN + 1))
|
|
1096
|
+
continue
|
|
1097
|
+
fi
|
|
1098
|
+
|
|
1099
|
+
# Write ADR file
|
|
1100
|
+
PR_REF="${PR_NUMBER:-unknown}"
|
|
1101
|
+
cat > "$ADR_PATH" <<ADR_EOF
|
|
1102
|
+
---
|
|
1103
|
+
issue: {NUMBER}
|
|
1104
|
+
pr: ${PR_REF}
|
|
1105
|
+
commit: ${COMMIT_SHA}
|
|
1106
|
+
status: fresh
|
|
1107
|
+
anchor: ${ANCHOR_PATH:-unknown}
|
|
1108
|
+
created: $(date -u +%Y-%m-%d)
|
|
1109
|
+
---
|
|
1110
|
+
|
|
1111
|
+
# ADR — ${text}
|
|
1112
|
+
|
|
1113
|
+
## Decision
|
|
1114
|
+
|
|
1115
|
+
${text}
|
|
1116
|
+
|
|
1117
|
+
## Context
|
|
1118
|
+
|
|
1119
|
+
Auto-extracted from FORGE:TRAJECTORY Decisions section on issue #{NUMBER}.
|
|
1120
|
+
|
|
1121
|
+
**Citations**:
|
|
1122
|
+
- Issue: https://github.com/{GH_REPO}/issues/{NUMBER}
|
|
1123
|
+
- PR: https://github.com/{GH_REPO}/pull/${PR_REF}
|
|
1124
|
+
- Commit: ${COMMIT_SHA}
|
|
1125
|
+
- Anchor: \`${ANCHOR_PATH:-no file anchor found}\`
|
|
1126
|
+
|
|
1127
|
+
## Status
|
|
1128
|
+
|
|
1129
|
+
\`fresh\` — anchor is active. Architect plans on future runs will inject this ADR as a constraint
|
|
1130
|
+
when the anchor path overlaps the contract files.
|
|
1131
|
+
|
|
1132
|
+
Set \`status: needs-review\` manually (or the staleness pass in \`build-knowledge-index.mjs\` will
|
|
1133
|
+
flip it automatically) when the anchored code region no longer exists.
|
|
1134
|
+
ADR_EOF
|
|
1135
|
+
|
|
1136
|
+
echo "[ADR] Written: $ADR_FILENAME"
|
|
1137
|
+
ADR_FILES_WRITTEN=$((ADR_FILES_WRITTEN + 1))
|
|
1138
|
+
done <<< "$DECISIONS_RAW"
|
|
1139
|
+
fi
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
### Step 4: Commit and push ADR files (non-blocking)
|
|
1143
|
+
|
|
1144
|
+
```bash
|
|
1145
|
+
if [ "$ADR_FILES_WRITTEN" -gt 0 ] && [ -n "{WORKTREE_PATH}" ] && [ -d "{WORKTREE_PATH}" ]; then
|
|
1146
|
+
# Commit ADR files in the worktree so they ride the existing PR
|
|
1147
|
+
(
|
|
1148
|
+
cd "{WORKTREE_PATH}"
|
|
1149
|
+
# Stage only the new/updated ADR files (not the whole tree)
|
|
1150
|
+
git add devdocs/decisions/*.md 2>/dev/null || true
|
|
1151
|
+
# Check if there's anything to commit
|
|
1152
|
+
if ! git diff --cached --quiet 2>/dev/null; then
|
|
1153
|
+
git commit -s -m "docs(decisions): auto-ADRs from TRAJECTORY decisions (#{NUMBER})" \
|
|
1154
|
+
--no-verify 2>/dev/null && echo "[ADR] Committed ${ADR_FILES_WRITTEN} ADR file(s)" || \
|
|
1155
|
+
echo "[ADR] WARNING: commit failed — ADR files on disk but not in git"
|
|
1156
|
+
else
|
|
1157
|
+
echo "[ADR] No staged changes — ADR files may already be committed"
|
|
1158
|
+
fi
|
|
1159
|
+
) || echo "[ADR] WARNING: worktree operations failed — ADR files written but not committed"
|
|
1160
|
+
else
|
|
1161
|
+
if [ "$ADR_FILES_WRITTEN" -gt 0 ]; then
|
|
1162
|
+
echo "[ADR] No worktree available — ADR files written to repo root devdocs/decisions/ only"
|
|
1163
|
+
echo "[ADR] Manually commit devdocs/decisions/*.md if needed"
|
|
1164
|
+
fi
|
|
1165
|
+
fi
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
### Step 5: Post audit annotation
|
|
1169
|
+
|
|
1170
|
+
```bash
|
|
1171
|
+
if [ "${ADR_FILES_WRITTEN:-0}" -gt 0 ]; then
|
|
1172
|
+
gh issue comment {NUMBER} {GH_FLAG} --body "<!-- FORGE:ADR_EXTRACTED -->
|
|
1173
|
+
${ADR_FILES_WRITTEN} ADR file(s) auto-extracted from TRAJECTORY decisions and written to \`devdocs/decisions/\`.
|
|
1174
|
+
|
|
1175
|
+
Future architect runs will load matching ADRs as constraints when anchor paths overlap contract files.
|
|
1176
|
+
|
|
1177
|
+
ADRs are human-editable — update or remove them as the codebase evolves. The staleness pass in
|
|
1178
|
+
\`build-knowledge-index.mjs\` automatically flips \`status: needs-review\` when an anchor is dead.
|
|
1179
|
+
|
|
1180
|
+
<!-- FORGE:ADR_EXTRACTED:COMPLETE -->" 2>/dev/null || true
|
|
1181
|
+
else
|
|
1182
|
+
echo "[ADR] No tradeoff-shaped decisions found — no ADR files written"
|
|
1183
|
+
fi
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
---
|
|
1187
|
+
|
|
574
1188
|
## Phase C5.5: Graph Decision Record (MANDATORY when PR exists)
|
|
575
1189
|
|
|
576
1190
|
**Skip if**: `{PR_NUMBER}` is empty OR `<!-- FORGE:DECISION_RECORD -->` already posted on the PR.
|