forgedock 1.1.1 → 1.1.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.
Files changed (171) hide show
  1. package/README.md +53 -8
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/demo.mjs +92 -7
  4. package/bin/engine/phases.mjs +20 -0
  5. package/bin/engine/state.mjs +8 -3
  6. package/bin/engine-cli.mjs +177 -5
  7. package/bin/forgedock.mjs +409 -26
  8. package/bin/hooks/interactive-engine.mjs +509 -0
  9. package/bin/hooks/pre-tool-use.mjs +498 -0
  10. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  11. package/bin/journey.mjs +72 -14
  12. package/bin/registry.mjs +139 -0
  13. package/bin/report.mjs +447 -0
  14. package/bin/runner.mjs +68 -19
  15. package/bin/settings-hook.mjs +235 -1
  16. package/commands/adopt.md +4 -1
  17. package/commands/analytics.md +4 -1
  18. package/commands/audit-agents.md +110 -27
  19. package/commands/audit.md +20 -1
  20. package/commands/autopilot.md +80 -8
  21. package/commands/changelog.md +2 -1
  22. package/commands/ci-audit.md +2 -1
  23. package/commands/cleanup.md +161 -1
  24. package/commands/compat-audit.md +2 -1
  25. package/commands/deploy-info.md +58 -1
  26. package/commands/design-bench.md +5 -1
  27. package/commands/design-render-critique-loop.md +1 -1
  28. package/commands/diagnose.md +2 -1
  29. package/commands/explain.md +2 -1
  30. package/commands/forge-stats.md +121 -2
  31. package/commands/forgedock-init.md +1 -1
  32. package/commands/geo-audit.md +3 -0
  33. package/commands/incident-response.md +2 -1
  34. package/commands/issue.md +23 -2
  35. package/commands/milestone.md +77 -12
  36. package/commands/optimize.md +2 -1
  37. package/commands/orchestrate/config.md +67 -0
  38. package/commands/orchestrate/phase-1-resolve.md +159 -0
  39. package/commands/orchestrate/phase-2-triage.md +126 -0
  40. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  41. package/commands/orchestrate/phase-3-dependency.md +591 -0
  42. package/commands/orchestrate/phase-4-execution.md +962 -0
  43. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  44. package/commands/orchestrate/phase-6-report.md +198 -0
  45. package/commands/orchestrate/safety.md +62 -0
  46. package/commands/orchestrate.md +25 -2045
  47. package/commands/pipeline-health/config.md +34 -0
  48. package/commands/pipeline-health/phase-1-context.md +180 -0
  49. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  50. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  51. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  52. package/commands/pipeline-health/phase-5-report.md +284 -0
  53. package/commands/pipeline-health/phase-6-summary.md +52 -0
  54. package/commands/pipeline-health.md +25 -2401
  55. package/commands/pipeline-resume.md +24 -1
  56. package/commands/pipeline-status.md +65 -0
  57. package/commands/qa-sweep.md +4 -1
  58. package/commands/quality-gate.md +248 -10
  59. package/commands/replay.md +2 -1
  60. package/commands/review-pr-agents/api.md +135 -0
  61. package/commands/review-pr-agents/auth.md +137 -0
  62. package/commands/review-pr-agents/billing.md +101 -0
  63. package/commands/review-pr-agents/concurrency.md +145 -0
  64. package/commands/review-pr-agents/database.md +103 -0
  65. package/commands/review-pr-agents/frontend.md +89 -0
  66. package/commands/review-pr-agents/infra.md +385 -0
  67. package/commands/review-pr-agents/protocols.md +185 -0
  68. package/commands/review-pr-agents/scraper.md +185 -0
  69. package/commands/review-pr-agents/security.md +406 -0
  70. package/commands/review-pr-agents.md +45 -1722
  71. package/commands/review-pr-staging.md +196 -40
  72. package/commands/review-pr.md +591 -52
  73. package/commands/rollback.md +2 -1
  74. package/commands/scope.md +2 -1
  75. package/commands/security-audit.md +2 -1
  76. package/commands/signal-planner.md +623 -0
  77. package/commands/sync-ecosystem.md +1 -0
  78. package/commands/test-gate.md +17 -1
  79. package/commands/validate.md +2 -1
  80. package/commands/work-on/build/architect.md +28 -0
  81. package/commands/work-on/build/context.md +2 -0
  82. package/commands/work-on/build/implement.md +3 -1
  83. package/commands/work-on/build/validate.md +26 -7
  84. package/commands/work-on/build.md +163 -24
  85. package/commands/work-on/close.md +130 -3
  86. package/commands/work-on/decompose.md +19 -1
  87. package/commands/work-on/investigate.md +117 -2
  88. package/commands/work-on/review.md +24 -4
  89. package/commands/work-on-monolithic.md +7 -2
  90. package/commands/work-on.md +202 -4
  91. package/docs/BRAND.md +14 -7
  92. package/docs/CI.md +2 -2
  93. package/docs/CODEX.md +36 -1
  94. package/docs/CONFIG.md +5 -8
  95. package/docs/FORGE-PROTOCOL.md +15 -1064
  96. package/docs/anthropic-devrel-outreach.md +2 -2
  97. package/docs/articles/command-decomposition-token-savings.md +214 -0
  98. package/docs/articles/model-release-playbook.md +131 -0
  99. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  100. package/docs/claude-breakpoints.json +75 -5
  101. package/docs/design/abc-benchmark.md +1 -1
  102. package/docs/design/design-spec-schema.md +1 -1
  103. package/docs/design/divergent-generation.md +1 -1
  104. package/docs/design/eval-gate-runbook.md +178 -0
  105. package/docs/design/show-hn-draft.md +26 -1
  106. package/docs/design/token-cost-baseline-1298.md +59 -0
  107. package/docs/eval/README.md +117 -0
  108. package/docs/eval/corpus/seed-v1.json +6 -0
  109. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  110. package/docs/site/annotations-explained.md +24 -0
  111. package/docs/site/command-learning-path.md +51 -14
  112. package/docs/site/command-reference.md +339 -113
  113. package/docs/site/for-companies.md +95 -0
  114. package/docs/site/forge-annotation-protocol.md +12 -256
  115. package/docs/site/getting-started.md +43 -4
  116. package/docs/site/github-is-the-memory.md +113 -0
  117. package/docs/site/how-it-works.md +3 -13
  118. package/docs/site/index.md +9 -1
  119. package/docs/spec/eval-run-result.md +147 -0
  120. package/docs/spec/forge-protocol-v1.md +441 -18
  121. package/docs/spec/label-state-machine.md +55 -0
  122. package/docs/spec/review-protocol.md +158 -0
  123. package/docs/spec-graph-schema.md +28 -0
  124. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  125. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  126. package/examples/forgedock-demo/README.md +41 -22
  127. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  128. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  129. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  130. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  131. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  132. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  133. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  134. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  135. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  136. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  137. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  138. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  139. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  140. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  141. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  142. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  143. package/examples/forgedock-demo/src/db.js +36 -11
  144. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  145. package/examples/forgedock-demo/src/server.js +4 -1
  146. package/package.json +3 -1
  147. package/packages/protocol/LICENSE +21 -0
  148. package/packages/protocol/src/cli.js +135 -0
  149. package/packages/protocol/src/emit.js +146 -0
  150. package/packages/protocol/src/index.js +43 -0
  151. package/packages/protocol/src/parse.js +229 -0
  152. package/packages/protocol/src/types.js +171 -0
  153. package/packages/protocol/src/validate.js +122 -0
  154. package/scripts/bench-topology-cost.mjs +372 -0
  155. package/scripts/check-command-docs-drift.sh +198 -0
  156. package/scripts/check-protocol-restatements.sh +176 -0
  157. package/scripts/check-registry/README.md +72 -0
  158. package/scripts/check-registry/manifest.json +6 -0
  159. package/scripts/code-index.sh +454 -0
  160. package/scripts/conformance-check.mjs +168 -0
  161. package/scripts/doctor-pipeline-state.sh +484 -0
  162. package/scripts/eval-gate-baseline.json +17 -0
  163. package/scripts/eval-gate-scorecard.mjs +242 -0
  164. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  165. package/scripts/flaky-quarantine.sh +241 -0
  166. package/scripts/forge-annotation.sh +567 -0
  167. package/scripts/issue-dedup.sh +236 -0
  168. package/scripts/validate-annotation-node.mjs +233 -0
  169. package/scripts/worktree-lifecycle.sh +3 -3
  170. package/templates/devdocs/agent/using-forgedock.md +14 -0
  171. package/docs/site/.vitepress/config.mjs +0 -85
@@ -0,0 +1,567 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios
3
+ # SPDX-License-Identifier: AGPL-3.0-or-later
4
+ #
5
+ # forge-annotation.sh — Deterministic FORGE annotation read/write/validate engine
6
+ #
7
+ # Every `commands/*.md` phase boundary posts or reads a machine-readable
8
+ # `<!-- FORGE:MARKER -->` HTML-comment annotation on a GitHub issue/PR. Until
9
+ # this script existed, each command spec re-derived the annotation body via
10
+ # inline heredoc/prose and re-derived the read/grep query independently, so
11
+ # annotation format drift between specs was possible and undetected. This
12
+ # script is the single source of truth for the annotation schema: it knows
13
+ # every marker's required fields and completion terminator, and exposes a
14
+ # stable write/read/validate CLI so pipeline phases stop hand-rolling both.
15
+ #
16
+ # Usage:
17
+ # forge-annotation.sh write <MARKER> [--field KEY=VALUE]... [--field-file KEY=FILE]...
18
+ # forge-annotation.sh read <ISSUE_NUMBER> [GH_FLAG...] <MARKER> [--all]
19
+ # forge-annotation.sh validate <MARKER> [FILE]
20
+ # forge-annotation.sh list-markers
21
+ # forge-annotation.sh -h | --help
22
+ #
23
+ # Subcommands:
24
+ # write Renders a well-formed `<!-- FORGE:MARKER -->` comment body to
25
+ # stdout, ready to be piped into `gh issue comment --body-file -`
26
+ # (or captured into a variable for `--body`). Fails (exit 1) if
27
+ # a required field for MARKER is missing.
28
+ # read Fetches the latest comment matching MARKER on ISSUE_NUMBER via
29
+ # `gh api` and prints its raw body to stdout. Prints nothing and
30
+ # exits 0 if no matching comment exists (mirrors the existing
31
+ # `| last | .body // ""` prose convention — "absent" is not an
32
+ # error). GH_FLAG tokens (e.g. `-R owner/repo`) may appear
33
+ # between ISSUE_NUMBER and MARKER, matching transition-label.sh's
34
+ # argument convention. Pass `--all` to print every matching
35
+ # comment body as a JSON array (newest last) instead of just the
36
+ # latest raw body.
37
+ # validate Reads a candidate comment body (from FILE, or stdin if FILE is
38
+ # omitted) and checks it has the correct `<!-- FORGE:MARKER -->`
39
+ # header and, if MARKER requires one, its completion terminator
40
+ # (e.g. `<!-- INVESTIGATION:COMPLETE -->` for INVESTIGATOR).
41
+ # Exits 1 with a clear message on any malformed body.
42
+ # list-markers Prints all known markers and their completion-terminator
43
+ # requirement (or "none").
44
+ #
45
+ # Exit codes: 0 = success, 1 = error (bad args, missing field, malformed body,
46
+ # gh/api failure, unknown marker).
47
+ #
48
+ # This is a UNIVERSAL-tier script (ships with the npm package). See
49
+ # devdocs/project/architecture.md → Script Precedence for the 4-tier
50
+ # resolution hierarchy this script participates in.
51
+
52
+ set -euo pipefail
53
+
54
+ SCRIPT_NAME="forge-annotation.sh"
55
+
56
+ # NOTE: this script does not export FORGEDOCK_SCRIPTS/FORGEDOCK_HOME. Those
57
+ # exports are meaningless when a script is invoked via `bash script.sh`
58
+ # (a subprocess) rather than sourced — the exported vars never reach the
59
+ # caller's shell. This exact dead-code pattern was confirmed and removed from
60
+ # classify-lane.sh in #852/#854; do not reintroduce it here.
61
+
62
+ usage() {
63
+ cat >&2 <<EOF
64
+ Usage:
65
+ $SCRIPT_NAME write <MARKER> [--field KEY=VALUE]... [--field-file KEY=FILE]...
66
+ $SCRIPT_NAME read <ISSUE_NUMBER> [GH_FLAG...] <MARKER> [--all]
67
+ $SCRIPT_NAME validate <MARKER> [FILE]
68
+ $SCRIPT_NAME list-markers
69
+ $SCRIPT_NAME -h | --help
70
+
71
+ Known markers: ${!COMPLETION_MARKER[*]} ${KNOWN_NO_TERMINATOR_MARKERS[*]}
72
+ EOF
73
+ }
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Marker registry
77
+ #
78
+ # COMPLETION_MARKER[MARKER] = literal completion-terminator string that MUST
79
+ # appear in the body for the marker to be
80
+ # considered COMPLETE. Absence from this map
81
+ # means the marker has no required terminator
82
+ # (it is a single-shot, always-complete post).
83
+ # PARTIAL_MARKER[MARKER] = literal alternate terminator accepted in place
84
+ # of the COMPLETE one when a phase intentionally
85
+ # posts a partial result under a time budget.
86
+ # ---------------------------------------------------------------------------
87
+ declare -A COMPLETION_MARKER=(
88
+ [INVESTIGATOR]="INVESTIGATION:COMPLETE"
89
+ [CONTEXT]="FORGE:CONTEXT:COMPLETE"
90
+ [ARCHITECT]="FORGE:ARCHITECT:COMPLETE"
91
+ [BUILDER]="FORGE:BUILDER:COMPLETE"
92
+ [DECOMPOSED]="FORGE:DECOMPOSED:COMPLETE"
93
+ )
94
+
95
+ declare -A PARTIAL_MARKER=(
96
+ [CONTEXT]="FORGE:CONTEXT:PARTIAL"
97
+ [ARCHITECT]="FORGE:ARCHITECT:PARTIAL"
98
+ )
99
+
100
+ # Markers with no required completion terminator (single-shot posts).
101
+ KNOWN_NO_TERMINATOR_MARKERS=(
102
+ CONTRACT CHECKPOINT FAST_PATH HEARTBEAT LEARNED TRAJECTORY
103
+ DECISION_RECORD REVIEW_STARTED
104
+ )
105
+
106
+ is_known_marker() {
107
+ local m="$1"
108
+ [[ -n "${COMPLETION_MARKER[$m]+x}" ]] && return 0
109
+ local known
110
+ for known in "${KNOWN_NO_TERMINATOR_MARKERS[@]}"; do
111
+ [[ "$known" == "$m" ]] && return 0
112
+ done
113
+ return 1
114
+ }
115
+
116
+ normalize_marker() {
117
+ # Accepts INVESTIGATOR, FORGE:INVESTIGATOR, or lowercase variants.
118
+ local raw="${1^^}"
119
+ raw="${raw#FORGE:}"
120
+ echo "$raw"
121
+ }
122
+
123
+ # ---------------------------------------------------------------------------
124
+ # Template registry — one function per marker, using {{FIELD}} placeholders.
125
+ # Substitution is plain bash parameter-expansion string replacement (handles
126
+ # multi-line replacement values fine, no sed/awk needed).
127
+ # ---------------------------------------------------------------------------
128
+
129
+ render_template() {
130
+ local marker="$1"
131
+ local body=""
132
+
133
+ case "$marker" in
134
+ INVESTIGATOR)
135
+ body='<!-- FORGE:INVESTIGATOR -->
136
+ ## Investigation Report
137
+
138
+ **Verdict**: {{VERDICT}}
139
+ **Confidence**: {{CONFIDENCE}}
140
+ **Severity**: {{SEVERITY}}
141
+ **Task Type**: {{TASK_TYPE}}
142
+
143
+ ### What Was Claimed
144
+ {{WHAT_CLAIMED}}
145
+
146
+ ### What We Found
147
+ {{WHAT_FOUND}}
148
+
149
+ ### Root Cause
150
+ {{ROOT_CAUSE}}
151
+
152
+ ### Affected Files
153
+ {{AFFECTED_FILES}}
154
+
155
+ ### Evidence
156
+ {{EVIDENCE}}
157
+
158
+ ### Recommendation
159
+ {{RECOMMENDATION}}
160
+
161
+ ### Related Issues
162
+ {{RELATED_ISSUES}}
163
+
164
+ ### Decomposition Assessment
165
+ **{{DECOMPOSE}}** — {{DECOMPOSE_REASON}}
166
+ {{DECOMPOSE_DETAILS}}
167
+
168
+ <!-- INVESTIGATION:COMPLETE -->'
169
+ ;;
170
+ CONTRACT)
171
+ body='<!-- FORGE:CONTRACT -->
172
+ ## Builder Contract
173
+
174
+ **Task type**: {{TASK_TYPE}}
175
+
176
+ ### Proposed Approach
177
+ {{APPROACH}}
178
+
179
+ ### Deliverables
180
+ | File | Change | Why |
181
+ |------|--------|-----|
182
+ {{DELIVERABLES_ROWS}}
183
+
184
+ ### Acceptance Criteria
185
+ {{ACCEPTANCE_CRITERIA}}
186
+
187
+ ### Quality Considerations
188
+ {{QUALITY_CONSIDERATIONS}}
189
+
190
+ ### Out of Scope
191
+ {{OUT_OF_SCOPE}}'
192
+ ;;
193
+ BUILDER)
194
+ body='<!-- FORGE:BUILDER -->
195
+ ## Implementation Complete
196
+
197
+ **Branch**: `{{BRANCH}}`
198
+ **Commits**: {{COMMITS}}
199
+ **Files changed**: {{FILES_CHANGED}}
200
+
201
+ ### Approach
202
+ {{APPROACH}}
203
+
204
+ ### Changes
205
+ {{CHANGES}}
206
+
207
+ ### Acceptance Criteria Status
208
+ {{ACCEPTANCE_STATUS}}
209
+
210
+ ### Testing Checklist
211
+ {{TESTING_CHECKLIST}}
212
+
213
+ <!-- FORGE:BUILDER:COMPLETE -->'
214
+ ;;
215
+ CHECKPOINT)
216
+ body='<!-- FORGE:CHECKPOINT -->
217
+ ```json
218
+ {"phase": "{{PHASE}}", "status": "{{STATUS}}", "next_phase": "{{NEXT_PHASE}}", "timestamp": "{{TIMESTAMP}}"}
219
+ ```'
220
+ ;;
221
+ *)
222
+ echo "ERROR: no write template registered for marker '$marker' (known write templates: INVESTIGATOR, CONTRACT, BUILDER, CHECKPOINT)" >&2
223
+ return 1
224
+ ;;
225
+ esac
226
+
227
+ echo "$body"
228
+ }
229
+
230
+ required_fields_for() {
231
+ case "$1" in
232
+ INVESTIGATOR) echo "VERDICT CONFIDENCE SEVERITY TASK_TYPE WHAT_CLAIMED WHAT_FOUND ROOT_CAUSE AFFECTED_FILES EVIDENCE RECOMMENDATION DECOMPOSE DECOMPOSE_REASON" ;;
233
+ CONTRACT) echo "TASK_TYPE APPROACH DELIVERABLES_ROWS ACCEPTANCE_CRITERIA" ;;
234
+ BUILDER) echo "BRANCH COMMITS FILES_CHANGED APPROACH CHANGES ACCEPTANCE_STATUS TESTING_CHECKLIST" ;;
235
+ CHECKPOINT) echo "PHASE STATUS NEXT_PHASE" ;;
236
+ *) echo "" ;;
237
+ esac
238
+ }
239
+
240
+ default_for_field() {
241
+ # Optional-field defaults, applied only when the field was not supplied.
242
+ local marker="$1" key="$2"
243
+ case "$marker:$key" in
244
+ INVESTIGATOR:RELATED_ISSUES) echo "None found." ;;
245
+ INVESTIGATOR:DECOMPOSE_DETAILS) echo "" ;;
246
+ CONTRACT:QUALITY_CONSIDERATIONS) echo "None." ;;
247
+ CONTRACT:OUT_OF_SCOPE) echo "None." ;;
248
+ CHECKPOINT:TIMESTAMP) date -u +"%Y-%m-%dT%H:%M:%SZ" ;;
249
+ *) echo "" ;;
250
+ esac
251
+ }
252
+
253
+ # ---------------------------------------------------------------------------
254
+ # write
255
+ # ---------------------------------------------------------------------------
256
+ cmd_write() {
257
+ if [ $# -lt 1 ]; then
258
+ echo "ERROR: write requires a MARKER argument" >&2
259
+ usage
260
+ exit 1
261
+ fi
262
+
263
+ local marker
264
+ marker="$(normalize_marker "$1")"
265
+ shift
266
+
267
+ case "$marker" in
268
+ INVESTIGATOR|CONTRACT|BUILDER|CHECKPOINT) ;;
269
+ *)
270
+ echo "ERROR: '$marker' has no registered write template. Supported: INVESTIGATOR, CONTRACT, BUILDER, CHECKPOINT" >&2
271
+ exit 1
272
+ ;;
273
+ esac
274
+
275
+ declare -A FIELDS=()
276
+
277
+ while [ $# -gt 0 ]; do
278
+ case "$1" in
279
+ --field)
280
+ if [ $# -lt 2 ] || [[ "$2" != *=* ]]; then
281
+ echo "ERROR: --field requires KEY=VALUE" >&2
282
+ exit 1
283
+ fi
284
+ local kv="$2" key val
285
+ key="${kv%%=*}"
286
+ val="${kv#*=}"
287
+ FIELDS["$key"]="$val"
288
+ shift 2
289
+ ;;
290
+ --field-file)
291
+ if [ $# -lt 2 ] || [[ "$2" != *=* ]]; then
292
+ echo "ERROR: --field-file requires KEY=FILE" >&2
293
+ exit 1
294
+ fi
295
+ local kvf="$2" fkey ffile
296
+ fkey="${kvf%%=*}"
297
+ ffile="${kvf#*=}"
298
+ if [ ! -f "$ffile" ]; then
299
+ echo "ERROR: --field-file: file not found: $ffile" >&2
300
+ exit 1
301
+ fi
302
+ FIELDS["$fkey"]="$(cat "$ffile")"
303
+ shift 2
304
+ ;;
305
+ *)
306
+ echo "ERROR: unknown write argument: $1" >&2
307
+ exit 1
308
+ ;;
309
+ esac
310
+ done
311
+
312
+ # Fill optional-field defaults for anything not explicitly supplied.
313
+ case "$marker" in
314
+ INVESTIGATOR)
315
+ for k in RELATED_ISSUES DECOMPOSE_DETAILS; do
316
+ [[ -n "${FIELDS[$k]+x}" ]] || FIELDS["$k"]="$(default_for_field "$marker" "$k")"
317
+ done
318
+ ;;
319
+ CONTRACT)
320
+ for k in QUALITY_CONSIDERATIONS OUT_OF_SCOPE; do
321
+ [[ -n "${FIELDS[$k]+x}" ]] || FIELDS["$k"]="$(default_for_field "$marker" "$k")"
322
+ done
323
+ ;;
324
+ CHECKPOINT)
325
+ [[ -n "${FIELDS[TIMESTAMP]+x}" ]] || FIELDS["TIMESTAMP"]="$(default_for_field "$marker" "TIMESTAMP")"
326
+ ;;
327
+ esac
328
+
329
+ # Validate required fields are present (and non-empty).
330
+ local missing="" f
331
+ for f in $(required_fields_for "$marker"); do
332
+ if [[ -z "${FIELDS[$f]+x}" ]] || [[ -z "${FIELDS[$f]}" ]]; then
333
+ missing="$missing $f"
334
+ fi
335
+ done
336
+ if [ -n "$missing" ]; then
337
+ echo "ERROR: missing required field(s) for MARKER '$marker':$missing" >&2
338
+ echo " Required fields: $(required_fields_for "$marker")" >&2
339
+ exit 1
340
+ fi
341
+
342
+ local body
343
+ body="$(render_template "$marker")" || exit 1
344
+
345
+ local key
346
+ for key in "${!FIELDS[@]}"; do
347
+ body="${body//\{\{$key\}\}/${FIELDS[$key]}}"
348
+ done
349
+
350
+ # Fail loudly if any placeholder was left unsubstituted — this catches
351
+ # template/field-name drift between this script and the callers of it.
352
+ if [[ "$body" == *"{{"*"}}"* ]]; then
353
+ echo "ERROR: unsubstituted placeholder(s) remain in rendered '$marker' body — check field names:" >&2
354
+ grep -oE '\{\{[A-Z_]+\}\}' <<<"$body" | sort -u >&2 || true
355
+ exit 1
356
+ fi
357
+
358
+ echo "$body"
359
+ }
360
+
361
+ # ---------------------------------------------------------------------------
362
+ # read
363
+ # ---------------------------------------------------------------------------
364
+ cmd_read() {
365
+ if [ $# -lt 2 ]; then
366
+ echo "ERROR: Usage: $SCRIPT_NAME read <ISSUE_NUMBER> [GH_FLAG...] <MARKER> [--all]" >&2
367
+ exit 1
368
+ fi
369
+
370
+ local issue_number="$1"
371
+ shift
372
+
373
+ if ! [[ "$issue_number" =~ ^[0-9]+$ ]]; then
374
+ echo "ERROR: ISSUE_NUMBER must be a positive integer, got: '$issue_number'" >&2
375
+ exit 1
376
+ fi
377
+
378
+ local all_flag=0
379
+ local args=("$@")
380
+ local filtered=()
381
+ local i
382
+ for ((i = 0; i < ${#args[@]}; i++)); do
383
+ if [ "${args[$i]}" = "--all" ]; then
384
+ all_flag=1
385
+ else
386
+ filtered+=("${args[$i]}")
387
+ fi
388
+ done
389
+
390
+ if [ ${#filtered[@]} -lt 1 ]; then
391
+ echo "ERROR: MARKER is required" >&2
392
+ exit 1
393
+ fi
394
+
395
+ local last_index=$(( ${#filtered[@]} - 1 ))
396
+ local marker
397
+ marker="$(normalize_marker "${filtered[$last_index]}")"
398
+ local gh_args=("${filtered[@]:0:$last_index}")
399
+
400
+ if ! is_known_marker "$marker"; then
401
+ echo "WARNING: '$marker' is not a known registered marker — reading anyway (unknown markers are not rejected, only unvalidated)." >&2
402
+ fi
403
+
404
+ # Resolve owner/repo for the gh api REST path. gh_args may contain `-R owner/repo`.
405
+ local repo=""
406
+ for ((i = 0; i < ${#gh_args[@]}; i++)); do
407
+ if [ "${gh_args[$i]}" = "-R" ]; then
408
+ if [ $((i + 1)) -ge ${#gh_args[@]} ]; then
409
+ echo "ERROR: -R requires a value <owner/repo>" >&2
410
+ echo "Usage: $SCRIPT_NAME read <ISSUE_NUMBER> [GH_FLAG...] <MARKER> [--all]" >&2
411
+ exit 1
412
+ fi
413
+ repo="${gh_args[$((i + 1))]}"
414
+ break
415
+ fi
416
+ done
417
+
418
+ if [ -z "$repo" ]; then
419
+ repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)"
420
+ if [ -z "$repo" ]; then
421
+ echo "ERROR: could not resolve owner/repo — pass -R <owner/repo> or run inside a repo with gh context" >&2
422
+ exit 1
423
+ fi
424
+ fi
425
+
426
+ if ! [[ "$repo" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then
427
+ echo "ERROR: resolved repo is not owner/repo format: '$repo'" >&2
428
+ exit 1
429
+ fi
430
+
431
+ local needle="FORGE:${marker}"
432
+ local comments_json
433
+ if ! comments_json="$(gh api "repos/${repo}/issues/${issue_number}/comments" --paginate 2>&1)"; then
434
+ echo "ERROR: gh api call failed: $comments_json" >&2
435
+ exit 1
436
+ fi
437
+
438
+ # `gh api --paginate` writes one JSON array PER PAGE to stdout, concatenated
439
+ # (not merged into a single top-level array). Issues with >100 comments span
440
+ # multiple pages. Feeding that multi-document stream directly into a jq filter
441
+ # written for one document makes jq re-run the filter once per page — the
442
+ # default/latest mode below would print one "latest per page" body per page
443
+ # instead of a single global latest, and --all mode would emit N separate
444
+ # JSON arrays concatenated instead of one valid array. Aggregate all pages
445
+ # into a single array first so behavior is correct regardless of page count;
446
+ # this is a no-op for the common single-page case.
447
+ local aggregated_json
448
+ if ! aggregated_json="$(jq -s 'add' <<<"$comments_json" 2>&1)"; then
449
+ echo "ERROR: failed to aggregate paginated comments: $aggregated_json" >&2
450
+ exit 1
451
+ fi
452
+ comments_json="$aggregated_json"
453
+
454
+ if [ "$all_flag" -eq 1 ]; then
455
+ jq --arg needle "$needle" '[.[] | select(.body | contains($needle)) | .body]' <<<"$comments_json"
456
+ else
457
+ jq -r --arg needle "$needle" '[.[] | select(.body | contains($needle))] | last | .body // ""' <<<"$comments_json"
458
+ fi
459
+ }
460
+
461
+ # ---------------------------------------------------------------------------
462
+ # validate
463
+ # ---------------------------------------------------------------------------
464
+ cmd_validate() {
465
+ if [ $# -lt 1 ]; then
466
+ echo "ERROR: Usage: $SCRIPT_NAME validate <MARKER> [FILE]" >&2
467
+ exit 1
468
+ fi
469
+
470
+ local marker
471
+ marker="$(normalize_marker "$1")"
472
+ shift
473
+
474
+ local body
475
+ if [ $# -ge 1 ]; then
476
+ if [ ! -f "$1" ]; then
477
+ echo "ERROR: file not found: $1" >&2
478
+ exit 1
479
+ fi
480
+ body="$(cat "$1")"
481
+ else
482
+ body="$(cat -)"
483
+ fi
484
+
485
+ if [ -z "$body" ]; then
486
+ echo "ERROR: empty body — nothing to validate" >&2
487
+ exit 1
488
+ fi
489
+
490
+ if ! is_known_marker "$marker"; then
491
+ echo "ERROR: unknown marker '$marker' — run 'list-markers' for the known set" >&2
492
+ exit 1
493
+ fi
494
+
495
+ local header="FORGE:${marker}"
496
+ if [[ "$body" != *"<!-- ${header} -->"* ]]; then
497
+ echo "ERROR: body is missing required header '<!-- ${header} -->'" >&2
498
+ exit 1
499
+ fi
500
+
501
+ if [[ -n "${COMPLETION_MARKER[$marker]+x}" ]]; then
502
+ local complete_marker="${COMPLETION_MARKER[$marker]}"
503
+ local partial_marker="${PARTIAL_MARKER[$marker]:-}"
504
+ if [[ "$body" == *"$complete_marker"* ]]; then
505
+ : # complete — OK
506
+ elif [[ -n "$partial_marker" ]] && [[ "$body" == *"$partial_marker"* ]]; then
507
+ echo "OK: '${marker}' annotation is well-formed (PARTIAL — terminator '${partial_marker}' present)"
508
+ return 0
509
+ else
510
+ echo "ERROR: '${marker}' annotation is missing its completion marker. Expected to find: <!-- ${complete_marker} -->" >&2
511
+ exit 1
512
+ fi
513
+ fi
514
+
515
+ if [ "$marker" = "CHECKPOINT" ]; then
516
+ local json_block
517
+ json_block="$(sed -n '/```json/,/```/p' <<<"$body" | sed '1d;$d')"
518
+ if [ -z "$json_block" ]; then
519
+ echo "ERROR: CHECKPOINT annotation is missing its \`\`\`json ... \`\`\` block" >&2
520
+ exit 1
521
+ fi
522
+ if ! echo "$json_block" | jq -e '.phase and .status and .next_phase and .timestamp' >/dev/null 2>&1; then
523
+ echo "ERROR: CHECKPOINT JSON block is missing one of the required keys: phase, status, next_phase, timestamp" >&2
524
+ exit 1
525
+ fi
526
+ fi
527
+
528
+ echo "OK: '${marker}' annotation is well-formed"
529
+ }
530
+
531
+ # ---------------------------------------------------------------------------
532
+ # list-markers
533
+ # ---------------------------------------------------------------------------
534
+ cmd_list_markers() {
535
+ echo "MARKER COMPLETION_TERMINATOR"
536
+ local m
537
+ for m in "${!COMPLETION_MARKER[@]}"; do
538
+ printf '%-20s %s\n' "$m" "${COMPLETION_MARKER[$m]}"
539
+ done | sort
540
+ for m in "${KNOWN_NO_TERMINATOR_MARKERS[@]}"; do
541
+ printf '%-20s %s\n' "$m" "none"
542
+ done | sort
543
+ }
544
+
545
+ # ---------------------------------------------------------------------------
546
+ # Entry point
547
+ # ---------------------------------------------------------------------------
548
+ if [ $# -lt 1 ]; then
549
+ usage
550
+ exit 1
551
+ fi
552
+
553
+ SUBCOMMAND="$1"
554
+ shift
555
+
556
+ case "$SUBCOMMAND" in
557
+ write) cmd_write "$@" ;;
558
+ read) cmd_read "$@" ;;
559
+ validate) cmd_validate "$@" ;;
560
+ list-markers) cmd_list_markers ;;
561
+ -h|--help) usage; exit 0 ;;
562
+ *)
563
+ echo "ERROR: unknown subcommand: $SUBCOMMAND" >&2
564
+ usage
565
+ exit 1
566
+ ;;
567
+ esac