forgedock 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/bin/engine/admission.mjs +57 -1
  2. package/bin/engine/admission.test.mjs +60 -0
  3. package/commands/analytics.md +3 -1
  4. package/commands/audit-agents.md +3 -1
  5. package/commands/audit.md +10 -8
  6. package/commands/autopilot.md +16 -14
  7. package/commands/ci-audit.md +4 -2
  8. package/commands/cleanup.md +1 -1
  9. package/commands/deploy-pr.md +31 -11
  10. package/commands/geo-audit.md +3 -1
  11. package/commands/incident-response.md +3 -1
  12. package/commands/issue.md +4 -2
  13. package/commands/milestone.md +8 -6
  14. package/commands/orchestrate/config.md +2 -2
  15. package/commands/orchestrate/phase-1-resolve.md +56 -12
  16. package/commands/orchestrate/phase-3-dependency.md +15 -5
  17. package/commands/orchestrate/phase-4-execution.md +593 -78
  18. package/commands/orchestrate/phase-5-cleanup.md +4 -2
  19. package/commands/orchestrate/phase-6-report.md +7 -3
  20. package/commands/orchestrate.md +13 -9
  21. package/commands/pipeline-health/phase-4-proposals.md +6 -4
  22. package/commands/pipeline-health/phase-5-report.md +4 -2
  23. package/commands/pipeline-health.md +9 -7
  24. package/commands/pipeline-resume.md +4 -2
  25. package/commands/qa-sweep.md +3 -1
  26. package/commands/quality-gate.md +4 -3
  27. package/commands/recover-orphans.md +172 -13
  28. package/commands/review-pr-agents.md +6 -4
  29. package/commands/review-pr-staging.md +13 -5
  30. package/commands/review-pr.md +187 -19
  31. package/commands/rollback.md +3 -1
  32. package/commands/security-audit.md +3 -1
  33. package/commands/signal-planner.md +6 -4
  34. package/commands/sync-ecosystem.md +4 -2
  35. package/commands/test-gate.md +4 -2
  36. package/commands/upgrade-deps.md +3 -1
  37. package/commands/validate.md +3 -1
  38. package/commands/work-on/build/architect.md +81 -19
  39. package/commands/work-on/build/context.md +61 -18
  40. package/commands/work-on/build/implement.md +62 -18
  41. package/commands/work-on/build/validate.md +240 -60
  42. package/commands/work-on/build.md +344 -146
  43. package/commands/work-on/close.md +617 -246
  44. package/commands/work-on/decompose.md +239 -41
  45. package/commands/work-on/investigate.md +377 -61
  46. package/commands/work-on/remediate.md +42 -9
  47. package/commands/work-on/review.md +240 -29
  48. package/commands/work-on-monolithic.md +48 -7
  49. package/commands/work-on.md +180 -2014
  50. package/docs/CODEX.md +2 -0
  51. package/docs/CONFIG.md +11 -0
  52. package/package.json +1 -1
  53. package/scripts/build-spec-graph.mjs +11 -4
  54. package/scripts/check-spec-markers.sh +11 -1
  55. package/scripts/checkbox-sections.test.sh +8 -23
  56. package/scripts/extract-affected-files.sh +5 -2
  57. package/scripts/extract-affected-files.test.sh +27 -0
  58. package/scripts/forge-root.test.sh +176 -0
  59. package/scripts/is-docs-only.sh +31 -0
  60. package/scripts/is-docs-only.test.sh +64 -0
  61. package/scripts/lint-dispatch-prompt.sh +217 -0
  62. package/scripts/lint-dispatch-prompt.test.sh +307 -0
  63. package/scripts/validate-spec-graph.sh +3 -2
  64. package/scripts/verify-phase-trail.sh +282 -0
  65. package/scripts/verify-phase-trail.test.sh +298 -0
  66. package/scripts/wait-ci-green.sh +98 -0
  67. package/scripts/wait-ci-green.test.sh +99 -0
@@ -121,6 +121,12 @@ export function canDeduplicateAutomatedAlert(canonical, candidate) {
121
121
  * unit before same-lineage refinements are deferred. `null` disables the bound.
122
122
  * @property {number} convergenceWindow - Merged-unit window used to warn when
123
123
  * amplification remains at or above 1.0.
124
+ * @property {boolean} amplificationBreaker - On-by-default circuit breaker over ALL
125
+ * cascade findings (not only same-lineage refinements): when the last
126
+ * `convergenceWindow` amplification ratios are all >= 1.0, P3-and-below admission
127
+ * pauses (P1/P2 are never affected). Opt out with `amplification_breaker: off`.
128
+ * It is deliberately independent of the cascade presets, so lifting the
129
+ * generation cap or token budget (`policy: all`) never disables it.
124
130
  */
125
131
 
126
132
  /**
@@ -179,6 +185,19 @@ export function parseOptionalPositiveNumber(raw) {
179
185
  };
180
186
  }
181
187
 
188
+ /**
189
+ * Parse an on/off toggle that defaults to ON. Accepts booleans and the strings
190
+ * "on"/"off"/"true"/"false". An unrecognized value keeps the safe default (on).
191
+ */
192
+ export function parseOnByDefaultToggle(raw) {
193
+ if (raw === undefined || raw === null || raw === "" || raw === "null") return { value: true, warning: null };
194
+ if (typeof raw === "boolean") return { value: raw, warning: null };
195
+ const t = String(raw).trim().toLowerCase();
196
+ if (t === "on" || t === "true") return { value: true, warning: null };
197
+ if (t === "off" || t === "false") return { value: false, warning: null };
198
+ return { value: true, warning: `not on/off ("${raw}") — keeping the breaker on` };
199
+ }
200
+
182
201
  /**
183
202
  * Parse a raw config value that may be a positive integer, the literal
184
203
  * string "unlimited" (case-insensitive), or absent/invalid. Mirrors the
@@ -229,6 +248,7 @@ export function parseIntOrUnlimited(raw, fallback) {
229
248
  * @param {boolean} [config.p3_same_file_defer]
230
249
  * @param {number|string} [config.max_amplification]
231
250
  * @param {number|string} [config.convergence_window]
251
+ * @param {boolean|string} [config.amplification_breaker] - "on" (default) | "off".
232
252
  * @param {number|string} [legacyTokenBudgetPerBatch] - Deprecated-alias fallback:
233
253
  * `pipeline.token_budget_per_batch`, read when `config.token_budget` is absent
234
254
  * so existing configs keep working unchanged (see forge#1858).
@@ -308,6 +328,11 @@ export function resolveCascadePolicy(config = {}, legacyTokenBudgetPerBatch) {
308
328
  );
309
329
  }
310
330
 
331
+ const amplificationBreaker = parseOnByDefaultToggle(config.amplification_breaker);
332
+ if (amplificationBreaker.warning) {
333
+ warnings.push(`orchestration.cascade.amplification_breaker ${amplificationBreaker.warning}`);
334
+ }
335
+
311
336
  // Both-uncapped notice: neither generation depth nor token spend is bounded this
312
337
  // run. This is never a preset default (no preset in CASCADE_PRESETS sets both to
313
338
  // UNLIMITED... except "all", which does so deliberately) — surface it loudly so an
@@ -332,6 +357,7 @@ export function resolveCascadePolicy(config = {}, legacyTokenBudgetPerBatch) {
332
357
  p3SameFileDefer,
333
358
  maxAmplification: maxAmplification.value,
334
359
  convergenceWindow: convergenceWindow.value === UNLIMITED ? 3 : convergenceWindow.value,
360
+ amplificationBreaker: amplificationBreaker.value,
335
361
  },
336
362
  policyName,
337
363
  bothUncapped,
@@ -387,6 +413,26 @@ export function evaluateAmplification(mergedUnits, findingsSpawned, policy) {
387
413
  };
388
414
  }
389
415
 
416
+ /**
417
+ * Evaluate the on-by-default amplification breaker. Counts every cascade finding
418
+ * (the ratio history is findings spawned / merged units, not refinement-only).
419
+ * Trips when the latest `convergenceWindow` observations are all >= 1.0; releases
420
+ * as soon as the newest observation falls below 1.0.
421
+ *
422
+ * @param {number[]} ratioHistory - Amplification ratios, oldest first.
423
+ * @param {CascadePolicy} policy
424
+ * @returns {{ tripped: boolean, ratio: number }}
425
+ */
426
+ export function evaluateAmplificationBreaker(ratioHistory, policy) {
427
+ const history = Array.isArray(ratioHistory) ? ratioHistory : [];
428
+ const ratio = history.length ? history[history.length - 1] : 0;
429
+ if (!policy.amplificationBreaker || history.length < policy.convergenceWindow) {
430
+ return { tripped: false, ratio };
431
+ }
432
+ const recent = history.slice(-policy.convergenceWindow);
433
+ return { tripped: recent.every((r) => r >= 1), ratio };
434
+ }
435
+
390
436
  /**
391
437
  * Evaluate the Step 4C rule chain for a single cascade-spawned finding.
392
438
  * Mirrors `commands/orchestrate/phase-4-execution.md` Step 4C's "Evaluation
@@ -405,6 +451,8 @@ export function evaluateAmplification(mergedUnits, findingsSpawned, policy) {
405
451
  * @param {boolean} finding.sameFileAsBatch
406
452
  * @param {boolean} finding.batchFullyGated
407
453
  * @param {number} finding.projectedTokenSpend
454
+ * @param {boolean} [finding.amplificationBreakerTripped] - Result of
455
+ * `evaluateAmplificationBreaker` for this batch (rule 6).
408
456
  * @param {CascadePolicy} policy
409
457
  * @returns {{ admit: boolean, reason: string|null }}
410
458
  */
@@ -418,7 +466,7 @@ export function evaluateCascadeFinding(finding, policy) {
418
466
  reason: `generation ${finding.generation} exceeds orchestration.cascade.max_generation (${policy.maxGeneration})`,
419
467
  };
420
468
  }
421
- if (finding.priority === "P1" || finding.priority === "P2") {
469
+ if (finding.priority === "P0" || finding.priority === "P1" || finding.priority === "P2") {
422
470
  return { admit: true, reason: null };
423
471
  }
424
472
  if (policy.keywordHeuristic && /comment|typo/i.test(finding.title || "")) {
@@ -430,6 +478,14 @@ export function evaluateCascadeFinding(finding, policy) {
430
478
  if (!admitsTokenSpend(finding.projectedTokenSpend, policy)) {
431
479
  return { admit: false, reason: `per-batch token budget exhausted (orchestration.cascade.token_budget=${policy.tokenBudget})` };
432
480
  }
481
+ // Rule 6 (forge#3060) is evaluated last, mirroring the bash chain. It only gates P3-and-below:
482
+ // P0/P1/P2 already returned above.
483
+ if (policy.amplificationBreaker && finding.amplificationBreakerTripped && /^P[3-9]$/.test(finding.priority || "")) {
484
+ return {
485
+ admit: false,
486
+ reason: "amplification breaker tripped — P3 admission paused (routed to P3 batches / completion sweep)",
487
+ };
488
+ }
433
489
  return { admit: true, reason: null };
434
490
  }
435
491
 
@@ -13,6 +13,7 @@ import {
13
13
  evaluateCascadeFinding,
14
14
  classifyBatchSafety,
15
15
  evaluateAmplification,
16
+ evaluateAmplificationBreaker,
16
17
  batchExclusionReason,
17
18
  planP3BatchGroups,
18
19
  canDeduplicateAutomatedAlert,
@@ -122,6 +123,7 @@ describe("resolveCascadePolicy — presets", () => {
122
123
  ...CASCADE_PRESETS.balanced,
123
124
  maxAmplification: null,
124
125
  convergenceWindow: 3,
126
+ amplificationBreaker: true,
125
127
  });
126
128
  assert.deepEqual(warnings, []);
127
129
  });
@@ -157,6 +159,7 @@ describe("resolveCascadePolicy — presets", () => {
157
159
  ...CASCADE_PRESETS.balanced,
158
160
  maxAmplification: null,
159
161
  convergenceWindow: 3,
162
+ amplificationBreaker: true,
160
163
  });
161
164
  assert.equal(warnings.length, 1);
162
165
  assert.match(warnings[0], /not one of/);
@@ -219,6 +222,63 @@ describe("evaluateAmplification", () => {
219
222
  });
220
223
  });
221
224
 
225
+ describe("evaluateAmplificationBreaker (forge#3060)", () => {
226
+ it("is on by default, including under policy: all", () => {
227
+ assert.equal(resolveCascadePolicy().policy.amplificationBreaker, true);
228
+ assert.equal(resolveCascadePolicy({ policy: "all" }).policy.amplificationBreaker, true);
229
+ });
230
+
231
+ it("opts out with off/false and keeps on for garbage with a warning", () => {
232
+ assert.equal(resolveCascadePolicy({ amplification_breaker: "off" }).policy.amplificationBreaker, false);
233
+ assert.equal(resolveCascadePolicy({ amplification_breaker: false }).policy.amplificationBreaker, false);
234
+ const r = resolveCascadePolicy({ amplification_breaker: "maybe" });
235
+ assert.equal(r.policy.amplificationBreaker, true);
236
+ assert.match(r.warnings[0], /amplification_breaker/);
237
+ });
238
+
239
+ it("trips only when the latest convergence_window ratios are all >= 1.0", () => {
240
+ const { policy } = resolveCascadePolicy();
241
+ assert.equal(evaluateAmplificationBreaker([1.5, 1.4], policy).tripped, false);
242
+ assert.equal(evaluateAmplificationBreaker([1.5, 1.4, 1.35], policy).tripped, true);
243
+ assert.equal(evaluateAmplificationBreaker([0.5, 1.4, 1.35], policy).tripped, false);
244
+ assert.equal(evaluateAmplificationBreaker([1.5, 1.4, 1.35, 0.9], policy).tripped, false);
245
+ });
246
+
247
+ it("never trips when opted out", () => {
248
+ const { policy } = resolveCascadePolicy({ amplification_breaker: "off" });
249
+ assert.equal(evaluateAmplificationBreaker([2, 2, 2], policy).tripped, false);
250
+ });
251
+
252
+ it("pauses P3 admission but never P1/P2, and ignores same-lineage max_amplification", () => {
253
+ const { policy } = resolveCascadePolicy({ policy: "all" });
254
+ const base = { generation: 1, title: "x", sameFileAsBatch: false, batchFullyGated: false, projectedTokenSpend: 0, amplificationBreakerTripped: true };
255
+ assert.equal(evaluateCascadeFinding({ ...base, priority: "P3" }, policy).admit, false);
256
+ assert.match(evaluateCascadeFinding({ ...base, priority: "P3" }, policy).reason, /amplification breaker/);
257
+ assert.equal(evaluateCascadeFinding({ ...base, priority: "P2" }, policy).admit, true);
258
+ assert.equal(evaluateCascadeFinding({ ...base, priority: "P1" }, policy).admit, true);
259
+ assert.equal(evaluateCascadeFinding({ ...base, priority: "P0" }, policy).admit, true);
260
+ assert.equal(evaluateCascadeFinding({ ...base, priority: "P3", amplificationBreakerTripped: false }, policy).admit, true);
261
+ });
262
+
263
+ it("rule 6 never gates untriaged (empty/unknown) priorities", () => {
264
+ const { policy } = resolveCascadePolicy();
265
+ const f = { generation: 1, priority: "", title: "x", sameFileAsBatch: false, batchFullyGated: false, projectedTokenSpend: 0, amplificationBreakerTripped: true };
266
+ assert.equal(evaluateCascadeFinding(f, policy).admit, true);
267
+ });
268
+
269
+ it("rule 6 is evaluated last: a tripped typo P3 reports the keyword reason first under balanced", () => {
270
+ const { policy } = resolveCascadePolicy();
271
+ const f = { generation: 1, priority: "P3", title: "fix typo", sameFileAsBatch: false, batchFullyGated: false, projectedTokenSpend: 0, amplificationBreakerTripped: true };
272
+ assert.match(evaluateCascadeFinding(f, policy).reason, /comment\/typo/);
273
+ });
274
+
275
+ it("existing numeric max_amplification configs keep working alongside the breaker", () => {
276
+ const { policy } = resolveCascadePolicy({ max_amplification: 1.5 });
277
+ assert.equal(policy.maxAmplification, 1.5);
278
+ assert.equal(policy.amplificationBreaker, true);
279
+ });
280
+ });
281
+
222
282
  describe("resolveCascadePolicy — token_budget deprecated-alias fallback", () => {
223
283
  it("falls back to legacy pipeline.token_budget_per_batch when orchestration.cascade.token_budget is absent", () => {
224
284
  const { policy } = resolveCascadePolicy({ policy: "balanced" }, 300000);
@@ -7,6 +7,8 @@ install: extras
7
7
 
8
8
  # /analytics — Production Analytics Audit & Issue Generator
9
9
 
10
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
11
+
10
12
  You are the analytics orchestrator. Pull data from ALL available analytics platforms (Google Search Console, Bing Webmaster, Microsoft Clarity, Umami, Cloudflare, Google Analytics 4), cross-reference the findings, generate actionable insights, and decompose them into concrete GitHub issues that the `/work-on` pipeline can pick up.
11
13
 
12
14
  **You have access to ALL tools** — MCP tools for GSC/Clarity/Stripe, Bash for Umami/Cloudflare/Bing APIs, Agent tool for parallel data collection.
@@ -728,7 +730,7 @@ BODY_EOF
728
730
  ```
729
731
 
730
732
  ```
731
- Skill(skill="issue", args="--title \"{type}: {action title}\" --body-file \"$ANALYTICS_BODY_FILE\" --label \"{priority}\" --label \"{category_label_1}\" --label \"{category_label_2}\"")
733
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{type}: {action title}\" --body-file \"$ANALYTICS_BODY_FILE\" --label \"{priority}\" --label \"{category_label_1}\" --label \"{category_label_2}\"")
732
734
  ```
733
735
 
734
736
  `{priority}`, `{category_label_1}`, and `{category_label_2}` are each passed as their own `--label` flag — `/issue`'s programmatic mode does not comma-split a single `--label` value. Resolve `{category_label_1}`/`{category_label_2}` from the label mapping below: split each mapping's comma-separated value into one label per placeholder. When a category maps to only one label, omit the `--label "{category_label_2}"` flag entirely (do not pass an empty string).
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /audit-agents — Agent Output Auditor
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  ## Purpose
@@ -511,7 +513,7 @@ N/A — this is a running log issue, not a code change.
511
513
  This issue is a running log of persisted `/audit-agents` summaries. Each comment contains one session's efficiency metrics. Do not close this issue — `/pipeline-health` Phase 2K queries it to aggregate orchestration efficiency trends.
512
514
  EOF
513
515
 
514
- Skill(skill="issue", args="--title \"Orchestration Metrics — Running Log\" --body-file \"$TRACKING_BODY_FILE\" --label \"orchestration-metrics\"")
516
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"Orchestration Metrics — Running Log\" --body-file \"$TRACKING_BODY_FILE\" --label \"orchestration-metrics\"")
515
517
 
516
518
  # /issue's programmatic mode does not print a structured return value — re-query
517
519
  # by label to recover the newly created issue number.
package/commands/audit.md CHANGED
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /audit — Pipeline Failure Trace & Self-Healing Issue
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  You are Forge's post-mortem auditor. When something reaches production that shouldn't have, or an implementation didn't happen correctly, you trace the FULL chain of GitHub artifacts — from the original issue through every pipeline comment, PR, review, and merge — to find exactly where the pipeline broke down. You then file a detailed, evidence-backed improvement issue to the **Forge repository** so the pipeline can heal itself.
@@ -240,16 +242,16 @@ Classify where the breakdown occurred. Use these categories:
240
242
 
241
243
  | Failure Point | Description | Fix Target |
242
244
  |---------------|-------------|------------|
243
- | **INVESTIGATION** | Wrong verdict, missed scope, insufficient analysis | `commands/work-on.md` Phase 1 |
244
- | **DECOMPOSITION** | Should have decomposed but didn't, or decomposed wrong | `commands/work-on.md` Phase 2 |
245
- | **CONTRACT** | Wrong deliverables, missing acceptance criteria | `commands/work-on.md` Phase 3C |
246
- | **CONTEXT** | Missed relevant prior work, bug patterns, or related code | `commands/work-on.md` Phase 3C.5 |
247
- | **ARCHITECT** | Missed code paths, wrong implementation plan | `commands/work-on.md` Phase 3C.6 |
248
- | **IMPLEMENTATION** | Code written incorrectly, incomplete, wrong approach | `commands/work-on.md` Phase 3F, builder rules |
245
+ | **INVESTIGATION** | Wrong verdict, missed scope, insufficient analysis | `commands/work-on/investigate.md` |
246
+ | **DECOMPOSITION** | Should have decomposed but didn't, or decomposed wrong | `commands/work-on/decompose.md` |
247
+ | **CONTRACT** | Wrong deliverables, missing acceptance criteria | `commands/work-on/build.md` (B2 Contract) |
248
+ | **CONTEXT** | Missed relevant prior work, bug patterns, or related code | `commands/work-on/build/context.md` |
249
+ | **ARCHITECT** | Missed code paths, wrong implementation plan | `commands/work-on/build/architect.md` |
250
+ | **IMPLEMENTATION** | Code written incorrectly, incomplete, wrong approach | `commands/work-on/build/implement.md` (I3 rules) |
249
251
  | **QUALITY_GATE** | Should have caught this defect class but didn't | `commands/quality-gate.md` |
250
252
  | **REVIEW** | Review agents missed the issue, or wrong agents were triggered | `commands/review-pr.md`, `commands/review-pr-agents.md` |
251
253
  | **REVIEW_FALSE_NEG** | Review agent looked at the right area but didn't flag the problem | `commands/review-pr-agents.md` (agent template) |
252
- | **MERGE_POLICY** | Merged despite signals that should have blocked | `commands/work-on.md` Phase 5 |
254
+ | **MERGE_POLICY** | Merged despite signals that should have blocked | `commands/work-on/review.md` + `commands/review-pr.md` Phase 8 |
253
255
  | **DEPLOY_GATE** | Deploy-info should have flagged risk but didn't | `commands/deploy-info.md` |
254
256
  | **ORCHESTRATION** | Wave ordering wrong, dependency missed, stall | `commands/orchestrate.md` |
255
257
  | **ISSUE_SPEC** | Original issue was ambiguous/incomplete — pipeline followed it correctly but the spec was wrong | Not a pipeline fix — note for process improvement |
@@ -384,7 +386,7 @@ EOF
384
386
  ```
385
387
 
386
388
  ```
387
- Skill(skill="issue", args="--title \"$AUDIT_TITLE\" --body-file \"$AUDIT_BODY_FILE\" --label \"{LABEL_1}\" --label \"{LABEL_2}\" --label \"{LABEL_3}\"")
389
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$AUDIT_TITLE\" --body-file \"$AUDIT_BODY_FILE\" --label \"{LABEL_1}\" --label \"{LABEL_2}\" --label \"{LABEL_3}\"")
388
390
  ```
389
391
 
390
392
  `{LABELS}` from the table in 4A (e.g. `P1,audit-finding,bug`) is split into one `--label` flag per label (`{LABEL_1}`, `{LABEL_2}`, `{LABEL_3}`) — `/issue`'s programmatic mode does not comma-split a single `--label` value. If `/issue` stops on dedup (near-duplicate or usage error, per the note above), do not fall back to a raw `gh issue create` — comment on the existing issue with the new evidence instead.
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /autopilot — Autonomous Deploy Loop
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS (default: full autonomous loop until zero open issues remain)
12
14
 
13
15
  **Config variables used by this command** (set in `forge.yaml`):
@@ -160,7 +162,7 @@ This issue is the rolling log for `/autopilot` cycle annotations. Each cycle app
160
162
  OPS_EOF
161
163
  # Route through the /issue create-hook (canonical dedup + body validation) instead of
162
164
  # a raw `gh issue create`. See commands/issue.md Programmatic Invocation Contract.
163
- Skill(skill="issue", args="--title \"ops: autopilot cycle log\" --body-file \"$OPS_BODY_TMPFILE\" --label \"$AUTOPILOT_OPS_LABEL\"")
165
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"ops: autopilot cycle log\" --body-file \"$OPS_BODY_TMPFILE\" --label \"$AUTOPILOT_OPS_LABEL\"")
164
166
  rm -f "$OPS_BODY_TMPFILE"
165
167
  trap - EXIT
166
168
  OPS_ISSUE_NUMBER=$(gh issue list $GH_FLAG \
@@ -403,7 +405,7 @@ RECOVER_ORPHANS_AVAILABLE=$(ls ~/.claude/commands/recover-orphans.md 2>/dev/null
403
405
  if [ "$INFLIGHT_ISSUES" -gt 0 ]; then
404
406
  if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ] && [ "$DRY_RUN" = "false" ]; then
405
407
  echo "Recovering $INFLIGHT_ISSUES orphaned pipeline issue(s)..."
406
- Skill("recover-orphans", args="")
408
+ Skill("{FORGE_SKILL_PREFIX}recover-orphans", args="")
407
409
  elif [ "$DRY_RUN" = "true" ]; then
408
410
  echo "[DRY-RUN] Would invoke: Skill(recover-orphans)"
409
411
  else
@@ -462,7 +464,7 @@ CLEANUP_AVAILABLE=$(ls ~/.claude/commands/cleanup.md 2>/dev/null && echo "true"
462
464
  if [ "$DRY_RUN" = "false" ]; then
463
465
  if [ "$CLEANUP_AVAILABLE" = "true" ]; then
464
466
  echo "Running label hygiene sweep via /cleanup..."
465
- Skill("cleanup", args="labels")
467
+ Skill("{FORGE_SKILL_PREFIX}cleanup", args="labels")
466
468
  CLEANUP_LABELS_FIXED=1 # /cleanup ran; detailed counts are in its own output
467
469
  else
468
470
  echo "INFO: /cleanup not installed (extras tier) — skipping label hygiene sweep. Install with: npx forgedock install --extras"
@@ -594,7 +596,7 @@ ISSUE_BODY_EOF
594
596
  # Route through the /issue create-hook (canonical dedup + body validation) instead of
595
597
  # a raw `gh issue create`. If /issue finds a near-duplicate, it reports it and does not
596
598
  # create — NEW_NUMBER will come back empty from the lookup below.
597
- Skill(skill="issue", args="--title \"$FINDING_TITLE\" --body-file \"$BODY_TMPFILE\" --label P2 --label bug")
599
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$FINDING_TITLE\" --body-file \"$BODY_TMPFILE\" --label P2 --label bug")
598
600
 
599
601
  rm -f "$BODY_TMPFILE"
600
602
  trap - EXIT
@@ -822,7 +824,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
822
824
  2>/dev/null | grep -q true; then
823
825
  echo " #$ISSUE_NUM ⟳ stalled at workflow:in-review (PR merged) — resuming via recover-orphans"
824
826
  if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ]; then
825
- Skill("recover-orphans", args="--issue $ISSUE_NUM")
827
+ Skill("{FORGE_SKILL_PREFIX}recover-orphans", args="--issue $ISSUE_NUM")
826
828
  else
827
829
  echo " WARNING: recover-orphans not installed — cannot auto-resume #$ISSUE_NUM"
828
830
  STALLED_ISSUES+=("$ISSUE_NUM")
@@ -858,7 +860,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
858
860
  2>/dev/null || echo '')
859
861
  for N in $PRE_DISPATCH_ISSUES; do DISPATCHED_ISSUES+=("$N"); done
860
862
 
861
- Skill("orchestrate", args="fast-lane --auto")
863
+ Skill("{FORGE_SKILL_PREFIX}orchestrate", args="fast-lane --auto")
862
864
 
863
865
  # Terminal-state verification after orchestrate (fallback path).
864
866
  # Re-query GitHub state for each pre-dispatch issue.
@@ -894,7 +896,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
894
896
  if [ "$STAGING_AHEAD_NOW" -gt 0 ]; then
895
897
  echo "staging is $STAGING_AHEAD_NOW commit(s) ahead — deploying via /deploy-pr..."
896
898
  if [ "$DRY_RUN" = "false" ]; then
897
- DEPLOY_RESULT=$(Skill("deploy-pr", args="staging"))
899
+ DEPLOY_RESULT=$(Skill("{FORGE_SKILL_PREFIX}deploy-pr", args="staging"))
898
900
  # Parse structured JSON result from deploy-pr
899
901
  DEPLOY_STATUS=$(echo "$DEPLOY_RESULT" | jq -r '.status // empty' 2>/dev/null || echo '')
900
902
  [ -z "$DEPLOY_STATUS" ] && DEPLOY_STATUS=$(echo "$DEPLOY_RESULT" | grep -oE '"status":"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"' || echo 'unknown')
@@ -909,7 +911,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
909
911
 
910
912
  # Step 3: Recover any newly orphaned issues before next iteration
911
913
  if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ] && [ "$DRY_RUN" = "false" ]; then
912
- Skill("recover-orphans", args="--since 2")
914
+ Skill("{FORGE_SKILL_PREFIX}recover-orphans", args="--since 2")
913
915
  elif [ "$DRY_RUN" = "true" ]; then
914
916
  echo "[DRY-RUN] Would invoke: Skill(recover-orphans, --since 2)"
915
917
  fi
@@ -1083,7 +1085,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
1083
1085
  elif echo "$ISSUE_LABEL_STR" | grep -q 'workflow:in-review'; then
1084
1086
  echo " #$ISSUE_NUM ⟳ stalled at workflow:in-review — resuming via recover-orphans"
1085
1087
  if [ "$RECOVER_ORPHANS_AVAILABLE" = "true" ]; then
1086
- Skill("recover-orphans", args="--issue $ISSUE_NUM")
1088
+ Skill("{FORGE_SKILL_PREFIX}recover-orphans", args="--issue $ISSUE_NUM")
1087
1089
  else
1088
1090
  echo " WARNING: recover-orphans not installed — cannot auto-resume #$ISSUE_NUM"
1089
1091
  STALLED_ISSUES+=("$ISSUE_NUM")
@@ -1112,7 +1114,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
1112
1114
  2>/dev/null || echo '')
1113
1115
  for N in $MS_PRE_DISPATCH; do DISPATCHED_ISSUES+=("$N"); done
1114
1116
 
1115
- Skill("orchestrate", args="milestone $MS_SLUG --auto")
1117
+ Skill("{FORGE_SKILL_PREFIX}orchestrate", args="milestone $MS_SLUG --auto")
1116
1118
 
1117
1119
  # Terminal-state verification after orchestrate fallback.
1118
1120
  echo "Verifying terminal state for $( echo "$MS_PRE_DISPATCH" | wc -w | tr -d ' ') dispatched milestone issue(s) (orchestrate path)..."
@@ -1150,7 +1152,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
1150
1152
  if [ "$MILESTONE_BRANCH_EXISTS" = "true" ]; then
1151
1153
  echo "Shipping $MILESTONE_BRANCH → staging..."
1152
1154
  if [ "$DRY_RUN" = "false" ]; then
1153
- MS_RESULT=$(Skill("deploy-pr", args="$MILESTONE_BRANCH"))
1155
+ MS_RESULT=$(Skill("{FORGE_SKILL_PREFIX}deploy-pr", args="$MILESTONE_BRANCH"))
1154
1156
  MS_STATUS=$(echo "$MS_RESULT" | jq -r '.status // empty' 2>/dev/null || echo '')
1155
1157
  [ -z "$MS_STATUS" ] && MS_STATUS=$(echo "$MS_RESULT" | grep -oE '"status":"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"' || echo 'unknown')
1156
1158
  echo "Milestone ship status: $MS_STATUS"
@@ -1163,7 +1165,7 @@ To approve: remove \`needs-human\` label from #${ISSUE_NUM} and re-run \`/autopi
1163
1165
  STAGING_AHEAD_MS=$(git rev-list --count "origin/${DEFAULT_BRANCH}..origin/${STAGING_BRANCH}" 2>/dev/null || echo '0')
1164
1166
  if [ "$STAGING_AHEAD_MS" -gt 0 ]; then
1165
1167
  echo "Milestone merged to staging — deploying staging → $DEFAULT_BRANCH..."
1166
- MAIN_RESULT=$(Skill("deploy-pr", args="staging"))
1168
+ MAIN_RESULT=$(Skill("{FORGE_SKILL_PREFIX}deploy-pr", args="staging"))
1167
1169
  MAIN_STATUS=$(echo "$MAIN_RESULT" | jq -r '.status // empty' 2>/dev/null || echo 'unknown')
1168
1170
  echo "Main deploy status: $MAIN_STATUS"
1169
1171
  [ "$MAIN_STATUS" = "merged" ] && FAST_LANE_DEPLOYS=$((FAST_LANE_DEPLOYS + 1))
@@ -1377,7 +1379,7 @@ META_EOF
1377
1379
  # Route through the /issue create-hook (canonical dedup + body validation) instead
1378
1380
  # of a raw `gh issue create`. The EXISTING_META search above already guards against
1379
1381
  # re-creating for the same pattern; /issue's own dedup is an additional safety net.
1380
- Skill(skill="issue", args="--title \"$META_TITLE\" --body-file \"$META_BODY_TMPFILE\" --label priority:P2")
1382
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$META_TITLE\" --body-file \"$META_BODY_TMPFILE\" --label priority:P2")
1381
1383
  rm -f "$META_BODY_TMPFILE"
1382
1384
  trap - EXIT
1383
1385
  META_NEW_NUMBER=$(gh issue list $GH_FLAG \
@@ -1591,7 +1593,7 @@ Rules are listed in **precedence order** — when two rules appear to conflict,
1591
1593
 
1592
1594
  **Rule 8: deploy-pr result is authoritative** — if status is not "merged", do not assume the deploy succeeded. Log and continue the loop.
1593
1595
 
1594
- **Rule 9: Terminal-state verification is mandatory after every dispatch batch.** After every `forgedock run-issue … wait` block or `Skill(orchestrate)` call, autopilot MUST query the actual GitHub label state for each dispatched issue. An issue whose process exits 0 but whose GitHub label is still `workflow:in-review` or `workflow:building` is NOT done — the close phase may have been interrupted. Stalls at `workflow:in-review` with a merged PR are the known recoverable class; autopilot resumes them via `Skill("recover-orphans", args="--issue N")` once before recording them as stalled. The Phase 4 report MUST emit a per-issue disposition table sourced from these GitHub-verified states — self-reported sub-process success is not sufficient. <!-- Added: forge#1751 -->
1596
+ **Rule 9: Terminal-state verification is mandatory after every dispatch batch.** After every `forgedock run-issue … wait` block or `Skill(orchestrate)` call, autopilot MUST query the actual GitHub label state for each dispatched issue. An issue whose process exits 0 but whose GitHub label is still `workflow:in-review` or `workflow:building` is NOT done — the close phase may have been interrupted. Stalls at `workflow:in-review` with a merged PR are the known recoverable class; autopilot resumes them via `Skill("{FORGE_SKILL_PREFIX}recover-orphans", args="--issue N")` once before recording them as stalled. The Phase 4 report MUST emit a per-issue disposition table sourced from these GitHub-verified states — self-reported sub-process success is not sufficient. <!-- Added: forge#1751 -->
1595
1597
 
1596
1598
  **Headless / unattended operation**: `/autopilot` has no human checkpoint and never waits for user input. When invoked via `/loop 4h /autopilot` or any other unattended runner, it runs to completion and exits. Human escalation is exclusively via the `needs-human` label (Rule 2 above) — autopilot surfaces `needs-human` issues in the recon report but never stalls waiting for a response. There is no "Phase 4B confirm before fixing" gate in the current design; that checkpoint was intentionally removed in the #1673 rewrite. If a future design adds a confirmation gate, it must be guarded by both Rule 0 (dry-run) and Rule 2 (needs-human) to remain safe.
1597
1599
 
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /ci-audit — Stack-Aware CI Gap Detection
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  You are the CI hygiene auditor. Given a project's declared tech stack (from `forge.yaml`), determine what config validation tools exist for each stack component and audit whether the project's GitHub Actions workflows include those validation steps. File GitHub issues for any gaps found.
@@ -370,7 +372,7 @@ ${VERSION_NOTE}
370
372
  # be unsafe to re-embed into a Skill args string.
371
373
  CI_GAP_BODY_FILE=$(mktemp)
372
374
  printf '%s' "$ISSUE_BODY" > "$CI_GAP_BODY_FILE"
373
- Skill(skill="issue", args="--title \"ci($tool): add structural config validation step to CI pipeline\" --body-file \"$CI_GAP_BODY_FILE\" --label \"bug\" --label \"P2\" --label \"audit-finding\"")
375
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"ci($tool): add structural config validation step to CI pipeline\" --body-file \"$CI_GAP_BODY_FILE\" --label \"bug\" --label \"P2\" --label \"audit-finding\"")
374
376
  fi
375
377
  fi
376
378
  done
@@ -414,7 +416,7 @@ The project has a \`$tool\` validation step in CI, but it only runs in the deplo
414
416
  # re-embed into a Skill args string.
415
417
  CI_PLACEMENT_BODY_FILE=$(mktemp)
416
418
  printf '%s' "$ISSUE_BODY" > "$CI_PLACEMENT_BODY_FILE"
417
- Skill(skill="issue", args="--title \"ci($tool): move config validation to PR-time CI workflow\" --body-file \"$CI_PLACEMENT_BODY_FILE\" --label \"bug\" --label \"P3\" --label \"audit-finding\"")
419
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"ci($tool): move config validation to PR-time CI workflow\" --body-file \"$CI_PLACEMENT_BODY_FILE\" --label \"bug\" --label \"P3\" --label \"audit-finding\"")
418
420
  fi
419
421
  done
420
422
  ```
@@ -267,7 +267,7 @@ echo "Removed: $WORKTREE_PATH ($BRANCH_NAME)"
267
267
  Delete remote `fix/` and `feat/` branches whose PR has merged — using **GitHub PR state as the source of truth**, not local git ancestry.
268
268
 
269
269
  **Why not `git branch -r --merged {STAGING_BRANCH}`**: an ancestry check only catches branches whose tip commit is reachable from `{STAGING_BRANCH}`. This misses two common cases:
270
- - **Feature-lane branches merged into a milestone branch.** Per `work-on.md` Phase 3E, milestone issues branch from and PR into `origin/milestone/{slug}`, not `{STAGING_BRANCH}`. Once such a branch's own PR merges into the milestone branch, it's fully absorbed and safe to delete — but it won't show up as "merged into staging" until the milestone itself ships (which may be days later, or never, if abandoned). This is the dominant cause of `feat/*` branches accumulating for a milestone/feature cluster.
270
+ - **Feature-lane branches merged into a milestone branch.** Per `work-on/build.md` (B1 worktree), milestone issues branch from and PR into `origin/milestone/{slug}`, not `{STAGING_BRANCH}`. Once such a branch's own PR merges into the milestone branch, it's fully absorbed and safe to delete — but it won't show up as "merged into staging" until the milestone itself ships (which may be days later, or never, if abandoned). This is the dominant cause of `feat/*` branches accumulating for a milestone/feature cluster.
271
271
  - **Squash merges.** If a branch was merged via squash (manual UI merge, or org/branch-protection settings that force squash-only), the resulting commit is a brand-new SHA that is never an ancestor of the original branch — so ancestry-based detection never matches it, even though the PR is clearly `MERGED` on GitHub.
272
272
 
273
273
  Both gaps disappear when merged-PR head-refs (regardless of base branch or merge strategy) are used directly as the deletion set:
@@ -8,6 +8,8 @@ allowed-tools: Bash, Read, Grep, Glob, Skill
8
8
 
9
9
  # /deploy-pr — PR Ship Orchestrator
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  **NEVER use plan mode (EnterPlanMode)** — it breaks execution context.
@@ -146,7 +148,7 @@ fi
146
148
  echo "Source: $SOURCE → Target: $PR_TARGET (prefix: $TITLE_PREFIX)"
147
149
  ```
148
150
 
149
- **Routing guard — staging→main**: When `SOURCE=staging` and `PR_TARGET=main`, the review gate (Phase 4) MUST route to `Skill("review-pr-staging", ...)` rather than the standard `review-pr`. This ensures the staging deploy review (comprehensive, multi-agent) runs instead of the single-PR review.
151
+ **Routing guard — staging→main**: When `SOURCE=staging` and `PR_TARGET=main`, the review gate (Phase 4) MUST route to `Skill("{FORGE_SKILL_PREFIX}review-pr-staging", ...)` rather than the standard `review-pr`. This ensures the staging deploy review (comprehensive, multi-agent) runs instead of the single-PR review.
150
152
 
151
153
  **Routing guard — milestone→staging**: When `SOURCE` matches `milestone/*`, this is a feature lane ship. `/review-pr` handles these normally (they target staging, not main).
152
154
 
@@ -285,12 +287,14 @@ FIX_CI_AVAILABLE=$(ls ~/.claude/commands/fix-ci.md 2>/dev/null && echo "true" ||
285
287
 
286
288
  if [ "$FIX_CI_AVAILABLE" = "false" ]; then
287
289
  echo "WARNING: /fix-ci not available — skipping CI gate. Install fix-ci (issue #1675) for automated CI fixing."
288
- echo "CI gate: SKIPPED (fix-ci unavailable)"
289
- CI_GATE_PASSED=true # Allow pipeline to continue; review gate will catch CI failures
290
+ echo "CI fix loop: SKIPPED (fix-ci unavailable) — the Phase 5 CI gate still blocks a red merge"
291
+ CI_GATE_PASSED=true # the fix loop is skipped, not the gate: Phase 5 runs wait-ci-green.sh before merging
290
292
  else
291
293
  # Check current CI status
292
- CURRENT_CI_STATUS=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,status,conclusion \
293
- --jq '[.[] | select(.conclusion == "failure")] | length' 2>/dev/null || echo "0")
294
+ # Fail closed: valid fields only (`status`/`conclusion` do not exist on gh pr checks --json; the
295
+ # old query errored and defaulted to 0 = "no failures"). An unreadable result counts as failing.
296
+ CURRENT_CI_STATUS=$(gh pr checks "$PR_NUMBER" $GH_FLAG --json name,bucket \
297
+ --jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")] | length' 2>/dev/null || echo "1")
294
298
 
295
299
  if [ "$CURRENT_CI_STATUS" -eq 0 ]; then
296
300
  echo "CI gate: PASSED (no failing checks)"
@@ -303,7 +307,7 @@ else
303
307
  echo "=== CI Gate Iteration $CI_ITER / $MAX_CI_ITER ==="
304
308
 
305
309
  if [ "$DRY_RUN" = "false" ]; then
306
- Skill("fix-ci", args="$PR_NUMBER $GH_FLAG")
310
+ Skill("{FORGE_SKILL_PREFIX}fix-ci", args="$PR_NUMBER $GH_FLAG")
307
311
  else
308
312
  echo "[DRY-RUN] Would invoke: Skill(fix-ci, $PR_NUMBER $GH_FLAG)"
309
313
  fi
@@ -356,9 +360,9 @@ while [ "$REVIEW_ITER" -lt "$MAX_REVIEW_ITER" ] && [ "$REVIEW_GATE_PASSED" = "fa
356
360
  if [ "$DRY_RUN" = "false" ]; then
357
361
  # Route: staging→main uses review-pr-staging; all other uses standard review-pr
358
362
  if [ "$SOURCE" = "$STAGING_BRANCH" ] && [ "$PR_TARGET" = "$DEFAULT_BRANCH" ]; then
359
- REVIEW_RESULT=$(Skill("review-pr-staging", args="$PR_NUMBER $GH_FLAG"))
363
+ REVIEW_RESULT=$(Skill("{FORGE_SKILL_PREFIX}review-pr-staging", args="$PR_NUMBER $GH_FLAG"))
360
364
  else
361
- REVIEW_RESULT=$(Skill("review-pr", args="$PR_NUMBER $GH_FLAG"))
365
+ REVIEW_RESULT=$(Skill("{FORGE_SKILL_PREFIX}review-pr", args="$PR_NUMBER $GH_FLAG"))
362
366
  fi
363
367
  else
364
368
  echo "[DRY-RUN] Would invoke review-pr for PR #$PR_NUMBER"
@@ -425,9 +429,25 @@ if [ "$DRY_RUN" = "true" ]; then
425
429
  echo "[DRY-RUN] Would merge PR #$PR_NUMBER (${SOURCE} → ${PR_TARGET})"
426
430
  MERGE_STATUS="dry_run"
427
431
  else
428
- echo "Merging PR #$PR_NUMBER..."
429
- MERGE_OUTPUT=$(gh pr merge "$PR_NUMBER" $GH_FLAG --merge 2>&1)
430
- MERGE_EXIT=$?
432
+ # CI gate (MANDATORY before any autonomous merge): wait for every check, merge only if all are
433
+ # green, and merge exactly the commit the gate checked.
434
+ _l="$(readlink -f "$HOME/.claude/commands/work-on.md" 2>/dev/null || true)"; _l="${_l%/commands/work-on.md}"
435
+ CI_GATE_SCRIPT=""
436
+ for _c in '${CLAUDE_PLUGIN_ROOT}' "${FORGE_ROOT:-}" "${FORGEDOCK_HOME:-}" "${FORGE_HOME:-}" "$_l"; do
437
+ case "$_c" in /*) [ -z "$CI_GATE_SCRIPT" ] && [ -f "$_c/scripts/wait-ci-green.sh" ] && CI_GATE_SCRIPT="$_c/scripts/wait-ci-green.sh" ;; esac
438
+ done
439
+ if [ -n "$CI_GATE_SCRIPT" ]; then CI_GATE_OUT=$(bash "$CI_GATE_SCRIPT" "$PR_NUMBER" $GH_FLAG); CI_GATE_RC=$?
440
+ else CI_GATE_OUT="CI_GATE: ERROR — scripts/wait-ci-green.sh not resolvable (fail closed)"; CI_GATE_RC=2; fi
441
+ echo "$CI_GATE_OUT"
442
+ GATED_HEAD=$(printf '%s\n' "$CI_GATE_OUT" | sed -n 's/^CI_GATE_HEAD: //p' | head -1)
443
+ # rc 3 = CI still running: re-run this block (up to 3 more times) before treating it as a failure.
444
+ if [ "$CI_GATE_RC" -ne 0 ]; then
445
+ MERGE_OUTPUT="not merged: CI gate rc=${CI_GATE_RC}"; MERGE_EXIT=1
446
+ else
447
+ echo "Merging PR #$PR_NUMBER..."
448
+ MERGE_OUTPUT=$(gh pr merge "$PR_NUMBER" $GH_FLAG --merge --match-head-commit "$GATED_HEAD" 2>&1)
449
+ MERGE_EXIT=$?
450
+ fi
431
451
 
432
452
  if [ "$MERGE_EXIT" -eq 0 ]; then
433
453
  echo "Merged PR #$PR_NUMBER successfully"
@@ -7,6 +7,8 @@ install: extras
7
7
 
8
8
  # /geo-audit — GEO Discoverability Audit & Issue Generator
9
9
 
10
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
11
+
10
12
  You are the GEO (Generative Engine Optimization) auditor. Pull AI referral data from Umami and Clarity, check every public page for GEO compliance (structured data, OG tags, freshness, sitemap, llms.txt), and auto-create GitHub issues for gaps.
11
13
 
12
14
  **You have access to ALL tools** — MCP tools for Clarity, Bash for Umami/curl checks, gh CLI for issues.
@@ -360,7 +362,7 @@ BODY_EOF
360
362
  ```
361
363
 
362
364
  ```
363
- Skill(skill="issue", args="--title \"{fix|feat}: {concise description}\" --body-file \"$GEO_BODY_FILE\" --label \"{priority}\" --label \"seo\" --label \"geo\"")
365
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"{fix|feat}: {concise description}\" --body-file \"$GEO_BODY_FILE\" --label \"{priority}\" --label \"seo\" --label \"geo\"")
364
366
  ```
365
367
 
366
368
  `{priority}`, `seo`, and `geo` are each passed as their own `--label` flag — `/issue`'s programmatic mode does not comma-split a single `--label` value. Every row in the Phase 3 table maps to the `seo,geo` label pair, so both are passed as separate literal `--label` flags rather than a single `{labels}` placeholder.
@@ -8,6 +8,8 @@ install: extras
8
8
 
9
9
  # /incident-response — P0 Incident Coordination
10
10
 
11
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
12
+
11
13
  **Input**: $ARGUMENTS
12
14
 
13
15
  **Config variables used by this command** (set in `forge.yaml`):
@@ -364,7 +366,7 @@ See postmortem: #{INCIDENT_NUMBER} (comment)
364
366
  BODY_EOF
365
367
  # Route through the /issue create-hook (canonical dedup + body validation) instead of a raw
366
368
  # `gh issue create`.
367
- Skill(skill="issue", args="--title \"$ACTION_ITEM_TITLE\" --body-file \"$ACTION_ITEM_BODY_TMPFILE\" --label \"{type_label}\" --label \"P{priority}\"")
369
+ Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"$ACTION_ITEM_TITLE\" --body-file \"$ACTION_ITEM_BODY_TMPFILE\" --label \"{type_label}\" --label \"P{priority}\"")
368
370
  rm -f "$ACTION_ITEM_BODY_TMPFILE"
369
371
  trap - EXIT
370
372
  ACTION_ITEM_NUMBER=$(gh issue list \
package/commands/issue.md CHANGED
@@ -7,13 +7,15 @@ argument-hint: "[description of the problem or feature] [--dry-run] | --title \"
7
7
 
8
8
  # /issue — Deterministic Issue Creator
9
9
 
10
+ > **Skill names**: `{FORGE_SKILL_PREFIX}` is `forgedock:` (plugin install) or empty (`install.sh`), resolved once per run by `commands/work-on.md` § Skill Name Resolution. If the skill is not found under either name, STOP and report "skill not found" — never run the phase inline.
11
+
10
12
  **Input**: $ARGUMENTS
11
13
 
12
14
  You create GitHub issues with the exact structure the `/work-on` pipeline expects. Every issue you create must give the investigation agent enough context to find the right files on the first pass — no vague descriptions, no missing domains, no ambiguous scope.
13
15
 
14
16
  `/issue` is the structured create-hook for issue creation across the pipeline — it enforces the canonical template, reads code before drafting, runs dedup, and validates mandatory sections. Because those checks are deterministic, `/issue` **creates the issue by default once they pass** — it does not wait for a human to approve the draft. Pass `--dry-run` to review the draft without creating anything (see Argument Parsing below).
15
17
 
16
- `/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="issue", args="--title \"fix: ...\" --body-file \"$BODY_FILE\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — every `--body-file` caller must use an entity- and agent-scoped scratch path and include a unique integrity marker which `/issue` verifies after creation. This is required because a collision can substitute plausible but unrelated content, not just produce an obvious file error (forge#2843).
18
+ `/issue` also accepts a **programmatic invocation form** for callers that have already composed a title, body, labels, and (optionally) a milestone — e.g. `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" --body-file \"$BODY_FILE\" --label bug --label P2")` (the caller writes the body to that path before invoking). This form skips the free-text parsing (Phase 1) and LLM drafting (Phase 3) entirely, but still runs the same dedup and body-validation correctness gates as the interactive path. See **Programmatic Invocation Contract** below — every `--body-file` caller must use an entity- and agent-scoped scratch path and include a unique integrity marker which `/issue` verifies after creation. This is required because a collision can substitute plausible but unrelated content, not just produce an obvious file error (forge#2843).
17
19
 
18
20
  **Agent model policy**: `model: "{DEFAULT_MODEL}"` — resolved from forge.yaml `agents.default_model`, else "sonnet" (standard tier). Fallback: `model: "opus"` if rate-limited. Feature gate: pass `effort` in Task/Skill spawns only on Claude Code >= 2.1.154.
19
21
  **NEVER use plan mode (EnterPlanMode).**
@@ -43,7 +45,7 @@ PROGRAMMATIC_MILESTONE=""
43
45
  PROGRAMMATIC_EXCLUDE=""
44
46
 
45
47
  # Positional/flag parsing. $ARGUMENTS is a single opaque string (the invoking
46
- # agent's raw args, e.g. from `Skill(skill="issue", args="--title \"fix: ...\" ...")`)
48
+ # agent's raw args, e.g. from `Skill(skill="{FORGE_SKILL_PREFIX}issue", args="--title \"fix: ...\" ...")`)
47
49
  # — it is NOT pre-tokenized by any harness, and a naive `ARGS=($ARGUMENTS)` would
48
50
  # word-split on whitespace without honoring embedded quote characters (a literal
49
51
  # `"` inside the string is not shell syntax at this point), fragmenting any