forgedock 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/engine/admission.mjs +57 -1
- package/bin/engine/admission.test.mjs +60 -0
- package/commands/analytics.md +3 -1
- package/commands/audit-agents.md +3 -1
- package/commands/audit.md +10 -8
- package/commands/autopilot.md +16 -14
- package/commands/ci-audit.md +4 -2
- package/commands/cleanup.md +1 -1
- package/commands/deploy-pr.md +31 -11
- package/commands/geo-audit.md +3 -1
- package/commands/incident-response.md +3 -1
- package/commands/issue.md +4 -2
- package/commands/milestone.md +8 -6
- package/commands/orchestrate/config.md +2 -2
- package/commands/orchestrate/phase-1-resolve.md +56 -12
- package/commands/orchestrate/phase-3-dependency.md +15 -5
- package/commands/orchestrate/phase-4-execution.md +593 -78
- package/commands/orchestrate/phase-5-cleanup.md +4 -2
- package/commands/orchestrate/phase-6-report.md +7 -3
- package/commands/orchestrate.md +13 -9
- package/commands/pipeline-health/phase-4-proposals.md +6 -4
- package/commands/pipeline-health/phase-5-report.md +4 -2
- package/commands/pipeline-health.md +9 -7
- package/commands/pipeline-resume.md +4 -2
- package/commands/qa-sweep.md +3 -1
- package/commands/quality-gate.md +4 -3
- package/commands/recover-orphans.md +172 -13
- package/commands/review-pr-agents.md +6 -4
- package/commands/review-pr-staging.md +13 -5
- package/commands/review-pr.md +187 -19
- package/commands/rollback.md +3 -1
- package/commands/security-audit.md +3 -1
- package/commands/signal-planner.md +6 -4
- package/commands/sync-ecosystem.md +4 -2
- package/commands/test-gate.md +4 -2
- package/commands/upgrade-deps.md +4 -2
- package/commands/validate.md +3 -1
- package/commands/work-on/build/architect.md +81 -19
- package/commands/work-on/build/context.md +61 -18
- package/commands/work-on/build/implement.md +62 -18
- package/commands/work-on/build/validate.md +240 -60
- package/commands/work-on/build.md +344 -146
- package/commands/work-on/close.md +617 -246
- package/commands/work-on/decompose.md +239 -41
- package/commands/work-on/investigate.md +377 -61
- package/commands/work-on/remediate.md +42 -9
- package/commands/work-on/review.md +240 -29
- package/commands/work-on-monolithic.md +48 -7
- package/commands/work-on.md +180 -2014
- package/docs/CODEX.md +2 -0
- package/docs/CONFIG.md +11 -0
- package/package.json +1 -1
- package/scripts/build-spec-graph.mjs +11 -4
- package/scripts/check-spec-markers.sh +11 -1
- package/scripts/checkbox-sections.test.sh +8 -23
- package/scripts/extract-affected-files.sh +5 -2
- package/scripts/extract-affected-files.test.sh +27 -0
- package/scripts/forge-root.test.sh +176 -0
- package/scripts/is-docs-only.sh +31 -0
- package/scripts/is-docs-only.test.sh +64 -0
- package/scripts/lint-dispatch-prompt.sh +217 -0
- package/scripts/lint-dispatch-prompt.test.sh +307 -0
- package/scripts/validate-spec-graph.sh +3 -2
- package/scripts/verify-phase-trail.sh +282 -0
- package/scripts/verify-phase-trail.test.sh +298 -0
- package/scripts/wait-ci-green.sh +98 -0
- package/scripts/wait-ci-green.test.sh +99 -0
package/bin/engine/admission.mjs
CHANGED
|
@@ -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);
|
package/commands/analytics.md
CHANGED
|
@@ -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).
|
package/commands/audit-agents.md
CHANGED
|
@@ -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`
|
|
244
|
-
| **DECOMPOSITION** | Should have decomposed but didn't, or decomposed wrong | `commands/work-on.md`
|
|
245
|
-
| **CONTRACT** | Wrong deliverables, missing acceptance criteria | `commands/work-on.md`
|
|
246
|
-
| **CONTEXT** | Missed relevant prior work, bug patterns, or related code | `commands/work-on.md`
|
|
247
|
-
| **ARCHITECT** | Missed code paths, wrong implementation plan | `commands/work-on.md`
|
|
248
|
-
| **IMPLEMENTATION** | Code written incorrectly, incomplete, wrong approach | `commands/work-on.md`
|
|
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
|
|
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.
|
package/commands/autopilot.md
CHANGED
|
@@ -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
|
|
package/commands/ci-audit.md
CHANGED
|
@@ -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
|
```
|
package/commands/cleanup.md
CHANGED
|
@@ -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`
|
|
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:
|
package/commands/deploy-pr.md
CHANGED
|
@@ -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
|
|
289
|
-
CI_GATE_PASSED=true #
|
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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"
|
package/commands/geo-audit.md
CHANGED
|
@@ -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
|