forgedock 1.0.21 → 1.1.1

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 (76) hide show
  1. package/README.md +227 -174
  2. package/bin/cinema.mjs +320 -0
  3. package/bin/demo.mjs +386 -0
  4. package/bin/engine/phases.mjs +148 -0
  5. package/bin/engine/projector.mjs +32 -0
  6. package/bin/engine/reconcile.mjs +11 -0
  7. package/bin/engine/runlog.mjs +79 -0
  8. package/bin/engine/state.mjs +27 -0
  9. package/bin/engine-cli.mjs +50 -0
  10. package/bin/engine.mjs +138 -0
  11. package/bin/forgedock.mjs +693 -1393
  12. package/bin/init-detect.mjs +95 -0
  13. package/bin/journey.mjs +783 -0
  14. package/bin/runner.mjs +847 -0
  15. package/bin/settings-hook.mjs +112 -0
  16. package/bin/tui.mjs +6 -3
  17. package/commands/autopilot.md +4 -4
  18. package/commands/cleanup.md +85 -39
  19. package/commands/deploy-info.md +11 -5
  20. package/commands/design-bench.md +1 -0
  21. package/commands/design-render-critique-loop.md +1 -0
  22. package/commands/design.md +1 -0
  23. package/commands/forge-stats.md +2 -1
  24. package/commands/issue.md +1 -1
  25. package/commands/optimize.md +1 -1
  26. package/commands/orchestrate.md +346 -34
  27. package/commands/quality-gate.md +168 -80
  28. package/commands/review-pr-agents.md +69 -45
  29. package/commands/review-pr.md +54 -6
  30. package/commands/scope.md +11 -3
  31. package/commands/security-audit.md +24 -16
  32. package/commands/sync-ecosystem.md +2 -2
  33. package/commands/work-on/build/architect.md +47 -3
  34. package/commands/work-on/build/context.md +36 -5
  35. package/commands/work-on/build/implement.md +16 -4
  36. package/commands/work-on/build/validate.md +54 -8
  37. package/commands/work-on/build.md +27 -9
  38. package/commands/work-on/close.md +131 -3
  39. package/commands/work-on/investigate.md +24 -1
  40. package/commands/work-on/review.md +24 -0
  41. package/commands/work-on-monolithic.md +1 -0
  42. package/commands/work-on.md +271 -11
  43. package/docs/BRAND.md +6 -2
  44. package/docs/CONFIG.md +39 -34
  45. package/docs/site/.vitepress/config.mjs +85 -0
  46. package/docs/site/annotations-explained.md +55 -0
  47. package/docs/site/command-learning-path.md +104 -0
  48. package/docs/site/command-reference.md +3 -0
  49. package/docs/site/getting-started.md +42 -13
  50. package/docs/site/index.md +1 -1
  51. package/docs/site/troubleshooting.md +208 -0
  52. package/docs/spec/forge-protocol-v1.md +768 -0
  53. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  54. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  55. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  56. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  57. package/examples/forgedock-demo/README.md +113 -0
  58. package/examples/forgedock-demo/bootstrap.sh +125 -0
  59. package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
  60. package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
  61. package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
  62. package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
  63. package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
  64. package/examples/forgedock-demo/labels.json +21 -0
  65. package/examples/forgedock-demo/package.json +16 -0
  66. package/examples/forgedock-demo/scripts/smoke.js +61 -0
  67. package/examples/forgedock-demo/src/auth.js +28 -0
  68. package/examples/forgedock-demo/src/db.js +66 -0
  69. package/examples/forgedock-demo/src/routes/notes.js +64 -0
  70. package/examples/forgedock-demo/src/server.js +79 -0
  71. package/package.json +13 -3
  72. package/scripts/verify-env-vars.sh +18 -2
  73. package/scripts/verify-host-headers.sh +4 -4
  74. package/scripts/verify-route-registration.sh +2 -2
  75. package/scripts/worktree-lifecycle.sh +202 -0
  76. package/templates/forge.yaml.minimal +44 -0
@@ -0,0 +1,112 @@
1
+ /**
2
+ * bin/settings-hook.mjs — Idempotent SessionStart hook registration in
3
+ * ~/.claude/settings.json.
4
+ *
5
+ * Contract (spec: "Hook wiring"):
6
+ * - Read-modify-write; unrelated keys and hooks preserved verbatim
7
+ * - Our entry identified by HOOK_MARKER in its command string
8
+ * - Malformed JSON → skip and report, NEVER overwrite the file
9
+ * - Absolute hook script path baked in at install time
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "fs";
13
+ import { dirname } from "path";
14
+
15
+ /** Substring identifying ForgeDock's own hook entry. */
16
+ export const HOOK_MARKER = "session-start.mjs";
17
+
18
+ function isOurs(entry) {
19
+ return JSON.stringify(entry).includes(HOOK_MARKER);
20
+ }
21
+
22
+ function readSettings(settingsPath) {
23
+ if (!existsSync(settingsPath)) return { settings: {}, fresh: true };
24
+ try {
25
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf-8"));
26
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
27
+ return { settings: parsed, fresh: false };
28
+ } catch {
29
+ return null; // malformed — caller must not write
30
+ }
31
+ }
32
+
33
+ function writeSettings(settingsPath, settings) {
34
+ mkdirSync(dirname(settingsPath), { recursive: true });
35
+ const tmp = settingsPath + ".tmp";
36
+ writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n", "utf-8");
37
+ renameSync(tmp, settingsPath);
38
+ }
39
+
40
+ /**
41
+ * Merge ForgeDock's SessionStart hook into settings.json.
42
+ * @param {string} settingsPath - Absolute path to ~/.claude/settings.json
43
+ * @param {string} hookScriptPath - Absolute path to bin/hooks/session-start.mjs
44
+ * @returns {{ status: 'installed'|'already'|'skipped-malformed' }}
45
+ */
46
+ export function installSessionStartHook(settingsPath, hookScriptPath) {
47
+ const read = readSettings(settingsPath);
48
+ if (read === null) return { status: "skipped-malformed" };
49
+ const { settings } = read;
50
+
51
+ // Validate hooks shape: must be a plain object (not array, string, etc.)
52
+ if (settings.hooks !== undefined && settings.hooks !== null) {
53
+ if (Array.isArray(settings.hooks) || typeof settings.hooks !== "object") {
54
+ return { status: "skipped-malformed" };
55
+ }
56
+ // Validate SessionStart if present: must be an array
57
+ if (settings.hooks.SessionStart !== undefined && !Array.isArray(settings.hooks.SessionStart)) {
58
+ return { status: "skipped-malformed" };
59
+ }
60
+ } else {
61
+ // hooks missing or null → initialize to fresh object
62
+ settings.hooks = {};
63
+ }
64
+
65
+ const list = Array.isArray(settings.hooks.SessionStart) ? settings.hooks.SessionStart : [];
66
+ if (list.some(isOurs)) return { status: "already" };
67
+
68
+ list.push({
69
+ hooks: [
70
+ {
71
+ type: "command",
72
+ command: `node "${hookScriptPath}"`,
73
+ },
74
+ ],
75
+ });
76
+ settings.hooks.SessionStart = list;
77
+ writeSettings(settingsPath, settings);
78
+ return { status: "installed" };
79
+ }
80
+
81
+ /**
82
+ * Remove ForgeDock's SessionStart hook entry; leaves everything else alone.
83
+ * @returns {{ status: 'removed'|'absent'|'skipped-malformed' }}
84
+ */
85
+ export function removeSessionStartHook(settingsPath) {
86
+ const read = readSettings(settingsPath);
87
+ if (read === null) return { status: "skipped-malformed" };
88
+ const { settings, fresh } = read;
89
+ if (fresh) return { status: "absent" };
90
+
91
+ // Validate hooks shape: must be a plain object (not array, string, etc.)
92
+ if (settings.hooks !== undefined && settings.hooks !== null) {
93
+ if (Array.isArray(settings.hooks) || typeof settings.hooks !== "object") {
94
+ return { status: "skipped-malformed" };
95
+ }
96
+ }
97
+
98
+ // Validate SessionStart shape if present: must be an array
99
+ if (settings.hooks && settings.hooks.SessionStart !== undefined && !Array.isArray(settings.hooks.SessionStart)) {
100
+ return { status: "skipped-malformed" };
101
+ }
102
+
103
+ const list = settings.hooks && Array.isArray(settings.hooks.SessionStart)
104
+ ? settings.hooks.SessionStart
105
+ : [];
106
+ const kept = list.filter((e) => !isOurs(e));
107
+ if (kept.length === list.length) return { status: "absent" };
108
+
109
+ settings.hooks.SessionStart = kept;
110
+ writeSettings(settingsPath, settings);
111
+ return { status: "removed" };
112
+ }
package/bin/tui.mjs CHANGED
@@ -837,7 +837,7 @@ const REVIEW_FIELDS = [
837
837
  */
838
838
  export async function annotatedReviewScreen(
839
839
  draft,
840
- { hasExistingConfig = false, existingContent = "", showSources = false } = {},
840
+ { hasExistingConfig = false, existingContent = "", showSources = false, extraFields = {} } = {},
841
841
  ) {
842
842
  // Helper — pull a field from the draft by path, or return a low-confidence placeholder.
843
843
  function getField(draftPath) {
@@ -862,7 +862,10 @@ export async function annotatedReviewScreen(
862
862
  const whys = {};
863
863
 
864
864
  for (const fd of REVIEW_FIELDS) {
865
- const field = getField(fd.draftPath);
865
+ const field =
866
+ fd.draftPath === null && extraFields[fd.key]
867
+ ? extraFields[fd.key]
868
+ : getField(fd.draftPath);
866
869
  values[fd.key] = field.value;
867
870
  confidences[fd.key] = field.confidence;
868
871
  sources[fd.key] = field.source;
@@ -987,7 +990,7 @@ export async function annotatedReviewScreen(
987
990
  }
988
991
 
989
992
  process.stdout.write(
990
- ` ${dim("Press")} ${bold("Enter")} ${dim("to accept all values, or enter a field number to edit it.")}\n\n`,
993
+ ` ${dim("Press")} ${bold("Enter")} ${dim("to forge, or a number to edit a field")}\n\n`,
991
994
  );
992
995
  }
993
996
 
@@ -10,7 +10,7 @@ argument-hint: [--fix | --recon-only | --fix --limit 5 | --dry-run]
10
10
  **Config variables used by this command** (set in `forge.yaml`):
11
11
  - `{CREDENTIALS_FILE}` ← `paths.credentials.file` (optional) — path to credentials YAML for analytics APIs
12
12
  - `{SERVER_SSH}` ← `services.server_ssh` (optional) — SSH target for production server health checks (e.g., `ubuntu@1.2.3.4`)
13
- - `{EMEMO_PATH}` ← `services.ememo_path` (optional) — path on production server to open eMemo files
13
+ - `{OPS_INBOX_PATH}` ← `services.ops_inbox_path` (optional) — path on production server to a directory of open work-item files (e.g. `/srv/ops/inbox`)
14
14
  - `{BILLING_ENABLED}` ← `billing.enabled` (optional, default `false`) — set to `true` to enable Stripe data collection in the Analytics Snapshot agent
15
15
 
16
16
  Resolve `{BILLING_ENABLED}` from `forge.yaml` before executing any phase:
@@ -112,7 +112,7 @@ Spawn these as **background agents** (all `run_in_background=true`, all `model="
112
112
  **Agent: Production Health**
113
113
  ```
114
114
  Check production system health:
115
- 1. SSH to check for open eMemos: if {SERVER_SSH} is configured, run: ssh {SERVER_SSH} "ls {EMEMO_PATH}/*-open-* 2>/dev/null" — skip this step if SERVER_SSH or EMEMO_PATH is not set in forge.yaml
115
+ 1. SSH to check for open ops-inbox items: if {SERVER_SSH} is configured, run: ssh {SERVER_SSH} "ls {OPS_INBOX_PATH}/*-open-* 2>/dev/null" — skip this step if SERVER_SSH or OPS_INBOX_PATH is not set in forge.yaml
116
116
  2. If any exist, cat each one and summarize
117
117
  3. Check container health via MCP: get_production_status, run_production_health_check
118
118
  4. Check recent error logs: get_production_logs for api and worker (last 100 lines), grep for ERROR/CRITICAL
@@ -197,7 +197,7 @@ For each finding from RECON_DATA, assign:
197
197
 
198
198
  | Signal | Priority | Type |
199
199
  |--------|----------|------|
200
- | Production eMemo (open) | P1 | bug |
200
+ | Open ops-inbox item | P1 | bug |
201
201
  | Container unhealthy | P0 | bug |
202
202
  | Error spike (>5x normal) | P1 | bug |
203
203
  | Tier degradation (>20% drop) | P2 | bug |
@@ -286,7 +286,7 @@ Print a structured report:
286
286
 
287
287
  ### Production Health
288
288
  - Containers: {all healthy / N unhealthy}
289
- - Open eMemos: {count}
289
+ - Open ops-inbox items: {count}
290
290
  - Error patterns: {summary or "none"}
291
291
 
292
292
  ### Scraping Performance
@@ -47,7 +47,7 @@ All `{GH_REPO}`, `{GH_FLAG}`, `{REPO_PATH}`, `{STAGING_BRANCH}`, `{PROJECT_BOARD
47
47
  | `labels` or empty | Fix stale/missing workflow labels on closed issues |
48
48
  | `orphans` | Close open issues whose PRs are already merged |
49
49
  | `branches` | Prune worktrees and remote branches for merged PRs |
50
- | `milestones` | Close milestones with 0 open issues |
50
+ | `milestones` | Report milestones with 0 open issues (advisory — never closes) |
51
51
  | `board` | Sync closed issues to Project board with correct terminal state |
52
52
  | `all` | All of the above, in order |
53
53
 
@@ -172,58 +172,109 @@ echo "Removed: $WORKTREE_PATH ($BRANCH_NAME)"
172
172
 
173
173
  ### 3C: Prune merged remote branches
174
174
 
175
- Delete remote `fix/` and `feat/` branches that are fully merged into `{STAGING_BRANCH}`:
175
+ Delete remote `fix/` and `feat/` branches whose PR has merged — using **GitHub PR state as the source of truth**, not local git ancestry.
176
+
177
+ **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:
178
+ - **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.
179
+ - **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.
180
+
181
+ Both gaps disappear when merged-PR head-refs (regardless of base branch or merge strategy) are used directly as the deletion set:
182
+
183
+ **Why also check `headRefOid`**: branch names are freely reusable in git. A name-only match cannot distinguish "this branch's current tip is the commit that merged" from "a branch with this name merged at some point in the past and has since been reused for new, unmerged work" (e.g. a second issue happens to slugify to the same `fix/*`/`feat/*` name). Comparing the branch's live remote tip SHA against the merged PR's `headRefOid` closes that gap — deletion only proceeds when the (name, SHA) pair matches a merged PR exactly, regardless of base branch or merge strategy.
184
+
185
+ **Why `--force-with-lease` on the delete**: the `CURRENT_SHA` snapshot above is read from the local `origin/$branch` ref as of the `git fetch --prune` at the top of this phase. For large batches this loop can run for a while (one network call per branch), so a new commit can land on the remote branch between the fetch and that branch's turn in the loop — a TOCTOU window. A plain `git push origin --delete "$branch"` is unconditional: it deletes whatever the remote ref currently points to, even if that's no longer `$CURRENT_SHA`. Using `--force-with-lease=refs/heads/$branch:$CURRENT_SHA` makes the deletion a server-verified compare-and-swap — the remote rejects the update if `refs/heads/$branch` has moved since the snapshot, instead of silently deleting new work.
176
186
 
177
187
  ```bash
178
188
  cd {REPO_PATH}
179
189
  git fetch --prune origin
180
190
 
181
- # Count first
182
- MERGED_COUNT=$(git branch -r --merged origin/{STAGING_BRANCH} | grep -E "origin/(fix|feat)/" | wc -l)
183
- echo "Found $MERGED_COUNT merged remote branches to delete"
191
+ # All remote fix/* and feat/* branches currently on origin
192
+ REMOTE_BRANCHES=$(git branch -r | grep -E "origin/(fix|feat)/" | sed 's|origin/||' | tr -d ' ')
193
+
194
+ # Head-ref name + head-ref SHA of every merged PR, regardless of base branch (staging,
195
+ # milestone/*, main) or merge strategy (merge-commit vs. squash) — this is GitHub's own
196
+ # merge bookkeeping, not local ref topology, so it's immune to both gaps above.
197
+ # headRefOid is captured alongside headRefName so deletion can be gated on the branch's
198
+ # CURRENT tip matching the commit that actually merged, not just a name match.
199
+ # --limit is set high to avoid silent truncation; repos with more historical merged PRs
200
+ # than this should re-run `/cleanup branches` incrementally to catch the remainder.
201
+ MERGED_HEADS=$(gh pr list {GH_FLAG} --state merged --limit 1000 --json headRefName,headRefOid --jq '.[] | "\(.headRefName)\t\(.headRefOid)"')
202
+
203
+ DELETE_COUNT=0
204
+ SKIP_COUNT=0
205
+ RACE_SKIP_COUNT=0
206
+ for branch in $REMOTE_BRANCHES; do
207
+ CURRENT_SHA=$(git rev-parse "origin/$branch" 2>/dev/null)
208
+ if printf '%s\n' "$MERGED_HEADS" | grep -qxF "$(printf '%s\t%s' "$branch" "$CURRENT_SHA")"; then
209
+ # Name AND tip SHA match a merged PR's head-ref — safe to delete.
210
+ # Use --force-with-lease as a server-side compare-and-swap: the remote re-verifies
211
+ # refs/heads/$branch is still at $CURRENT_SHA at push time, closing the TOCTOU gap
212
+ # between this snapshot and the actual delete (see note above).
213
+ if git push origin --force-with-lease="refs/heads/$branch:$CURRENT_SHA" ":refs/heads/$branch" 2>&1; then
214
+ DELETE_COUNT=$((DELETE_COUNT + 1))
215
+ # Log the pre-deletion SHA so it's visible in run output/logs for recovery —
216
+ # see "Recovery: restoring an accidentally pruned branch" below.
217
+ echo "Deleted origin/$branch (was $CURRENT_SHA) — restorable via: git push origin $CURRENT_SHA:refs/heads/$branch"
218
+ else
219
+ # Lease rejected — the branch's remote tip moved since the snapshot (a new push
220
+ # landed mid-loop). Do NOT retry/force past this: skip and let the next cleanup
221
+ # run re-evaluate it against fresh state.
222
+ RACE_SKIP_COUNT=$((RACE_SKIP_COUNT + 1))
223
+ echo "RACE: origin/$branch tip changed since snapshot ($CURRENT_SHA) — lease rejected, not deleting. Will re-evaluate on next /cleanup run."
224
+ fi
225
+ elif printf '%s\n' "$MERGED_HEADS" | cut -f1 | grep -qxF "$branch"; then
226
+ # Name matches a merged PR's head-ref, but the branch's current tip does not match
227
+ # any merged commit recorded under that name — likely a reused branch name holding
228
+ # new, unmerged work. Skip deletion rather than risk destroying live commits.
229
+ SKIP_COUNT=$((SKIP_COUNT + 1))
230
+ echo "SKIP: origin/$branch name matches a merged PR head-ref but current tip ($CURRENT_SHA) does not match the merged commit — branch name likely reused for new work. Not deleting."
231
+ fi
232
+ done
233
+ echo "Deleted $DELETE_COUNT merged remote branches, skipped $SKIP_COUNT name-matched/SHA-mismatched branches, skipped $RACE_SKIP_COUNT raced branches (tip changed between snapshot and delete) (source of truth: gh pr list --state merged, verified against headRefOid, deletion gated by --force-with-lease)"
234
+ ```
184
235
 
185
- # Delete them
186
- if [ "$MERGED_COUNT" -gt 0 ]; then
187
- git branch -r --merged origin/{STAGING_BRANCH} | grep -E "origin/(fix|feat)/" | sed 's|origin/||' | xargs -I{} git push origin --delete {} 2>&1
188
- fi
236
+ **Note**: This can take a while for large batches (1 network call per deleted branch, plus one batched `gh pr list` call). Run in background if > 20 branches.
237
+
238
+ **One-time backfill for existing stale branches**: repos that adopted this fix after already accumulating stale `feat/*`/`fix/*` branches (e.g. from milestones that shipped long ago) should run `/cleanup branches` once manually — the PR-state query above will catch the full backlog in a single pass since it isn't scoped to "this batch" or "this session," it queries all merged PRs on the repo.
239
+
240
+ **Recovery: restoring an accidentally pruned branch**
241
+
242
+ Phase 3C only ever deletes a branch whose (name, SHA) pair matched a merged PR's `headRefOid`, so the exact deleted commit is always recoverable: its SHA is echoed in the run log above (`Deleted origin/$branch (was $CURRENT_SHA) ...`), and the commit object survives in the remote's object store until garbage collection (for merge-commit merges it also remains reachable from the PR's merge commit; for squash merges the original tip becomes a dangling commit — not reachable by ancestry, but still restorable by SHA for as long as it hasn't been GC'd). If a branch is pruned in error (or needs to be recreated for any reason), restore it with:
243
+
244
+ ```bash
245
+ # Using the SHA logged at deletion time (or from `gh pr view {PR_NUMBER} --json headRefOid`
246
+ # if the run log is no longer available):
247
+ git push origin <sha>:refs/heads/<branch>
189
248
  ```
190
249
 
191
- **Note**: This can take a while for large batches (1 network call per branch). Run in background if > 20 branches.
250
+ Alternatively, GitHub itself offers a one-click **"Restore branch"** button on the merged PR's page (shown on the "<branch> was deleted" banner) for a limited window after deletion — typically available for as long as the underlying ref data hasn't been garbage-collected, often a few weeks. This is the fastest option when working from the GitHub UI rather than a local clone.
251
+
252
+ Both options only apply to branches deleted by this phase (or by GitHub's own merge/delete UI) — a SHA is not captured for branches removed by other means (e.g. manual `git push origin --delete` run outside of `/cleanup`), so recovery there depends on `git reflog` on a clone that still has the ref, or GitHub's audit log.
192
253
 
193
254
  ---
194
255
 
195
- ## Phase 4: Milestone Hygiene
256
+ ## Phase 4: Milestone Hygiene (Advisory Only)
257
+
258
+ **NEVER close milestones in this phase.** Milestones are only closed by `/milestone ship` (human-gated) or `/milestone close` (explicit abandonment). Incorrect closure destroys milestone state that cannot be trivially recovered. <!-- fix: forge#1160 -->
196
259
 
197
- ### 4A: Find completed milestones
260
+ ### 4A: Find milestones with 0 open issues
198
261
 
199
262
  ```bash
200
263
  # List open milestones with 0 remaining issues
201
264
  gh api repos/:owner/:repo/milestones --jq '.[] | select(.state == "open" and .open_issues == 0) | "\(.number) | \(.title) | open:\(.open_issues) closed:\(.closed_issues)"'
202
265
  ```
203
266
 
204
- ### 4B: Close completed milestones (shipping-verified only)
267
+ ### 4B: Report findings (DO NOT close)
205
268
 
206
- For each milestone with 0 open issues, verify the milestone branch has been merged to staging or main before closing:
269
+ For each milestone with 0 open issues, report it as a candidate for shipping or closing — but take no action:
207
270
 
208
271
  ```bash
209
- # Derive slug from milestone title (lowercase, spaces→hyphens, strip non-alphanumeric except hyphens)
210
- SLUG=$(echo "$MILESTONE_TITLE" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
211
-
212
- # Check if milestone branch was merged to staging or main
213
- SHIPPED_STAGING=$(gh pr list --base staging --head "milestone/$SLUG" --state merged --json number --jq '.[0].number' 2>/dev/null)
214
- SHIPPED_MAIN=$(gh pr list --base main --head "milestone/$SLUG" --state merged --json number --jq '.[0].number' 2>/dev/null)
215
-
216
- if [ -n "$SHIPPED_STAGING" ] || [ -n "$SHIPPED_MAIN" ]; then
217
- # Safe to close — code has been shipped to staging or main
218
- gh api repos/:owner/:repo/milestones/$MILESTONE_NUMBER -X PATCH -f state=closed
219
- echo "CLOSED milestone: $MILESTONE_TITLE (shipped via PR #${SHIPPED_STAGING:-$SHIPPED_MAIN})"
220
- else
221
- # DO NOT close — code is only on the milestone branch, not yet shipped
222
- echo "SKIPPED: $MILESTONE_TITLE — issues done but milestone branch not merged to staging or main"
223
- fi
272
+ echo "ADVISORY: $MILESTONE_TITLE — 0 open issues, $CLOSED_ISSUES closed"
273
+ echo " → To ship: /milestone ship $SLUG"
274
+ echo " → To abandon: /milestone close $SLUG"
224
275
  ```
225
276
 
226
- **Exception**: Don't close milestones that are intentionally kept open for future work (check if the milestone description says "ongoing" or "rolling"). If unsure, **SKIP it** — milestones are only closed by `/milestone ship` after code reaches staging. Incorrect closure destroys milestone state that cannot be trivially recovered.
277
+ Do NOT call `gh api ... -X PATCH -f state=closed` on any milestone. This phase is informational only.
227
278
 
228
279
  ---
229
280
 
@@ -281,15 +332,10 @@ fi
281
332
  | Local branches deleted | {N} |
282
333
  | Remote branches deleted | {N} |
283
334
 
284
- ### Milestones Closed
285
- | Milestone | Issues (closed) | Shipped via |
286
- |-----------|-----------------|-------------|
287
- | {title} | {N} | PR #{M} → staging/main |
288
-
289
- ### Milestones Skipped (unshipped)
290
- | Milestone | Issues (closed) | Reason |
291
- |-----------|-----------------|--------|
292
- | {title} | {N} | No merged PR from milestone/{slug} → staging or main |
335
+ ### Milestones Ready to Ship (advisory — no action taken)
336
+ | Milestone | Issues (closed) | Recommended Action |
337
+ |-----------|-----------------|-------------------|
338
+ | {title} | {N} | `/milestone ship {slug}` or `/milestone close {slug}` |
293
339
 
294
340
  ### Board Synced
295
341
  | Action | Count |
@@ -156,7 +156,7 @@ done
156
156
  # Full file diff summary
157
157
  git diff --stat origin/$TARGET..origin/$SOURCE
158
158
 
159
- # Read layout paths from forge.yaml, fall back to AlterLab defaults
159
+ # Read layout paths from forge.yaml, fall back to forge.yaml defaults
160
160
  LAYOUT_API=$(yq '.review.layout.api // "services/api"' forge.yaml 2>/dev/null || echo "services/api")
161
161
  LAYOUT_WORKER=$(yq '.review.layout.worker // "services/worker"' forge.yaml 2>/dev/null || echo "services/worker")
162
162
  LAYOUT_PAGES=$(yq '.review.layout.pages // "web/src/app"' forge.yaml 2>/dev/null || echo "web/src/app")
@@ -189,10 +189,14 @@ if [ -n "$MIGRATIONS" ]; then
189
189
  echo "⚠️ DATABASE MIGRATIONS INCLUDED:"
190
190
  echo "$MIGRATIONS"
191
191
  # Show migration content for review
192
- for m in $MIGRATIONS; do
192
+ # $MIGRATIONS is one path per line (`git diff --name-only`) — herestring,
193
+ # not a piped `| while read`, for consistency with the newline-safe pattern
194
+ # used elsewhere in this sweep.
195
+ while IFS= read -r m; do
196
+ [ -z "$m" ] && continue
193
197
  echo "--- $m ---"
194
198
  git show origin/$SOURCE:$m 2>/dev/null | head -30
195
- done
199
+ done <<< "$MIGRATIONS"
196
200
  fi
197
201
  ```
198
202
 
@@ -227,8 +231,10 @@ if [ -n "$NEW_ENV" ]; then
227
231
  # If your project uses a different backend, skip this block and verify manually.
228
232
  if [ "{DEPLOY_SECRETS_BACKEND}" = "sops" ]; then
229
233
  for var in $(echo "$NEW_ENV" | grep -oE '^\+[A-Z_]+' | sed 's/^\+//'); do
230
- if ! grep -q "$var" scripts/decrypt-secrets.sh 2>/dev/null; then
231
- echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
234
+ if [ -f "{REPO_PATH}/scripts/decrypt-secrets.sh" ]; then
235
+ grep -q "$var" "{REPO_PATH}/scripts/decrypt-secrets.sh" 2>/dev/null || echo " ❌ $var NOT in decrypt-secrets.sh ENV_MAPPING"
236
+ else
237
+ echo " ℹ️ scripts/decrypt-secrets.sh not found — skipping ENV_MAPPING cross-check for $var. Verify manually in your SOPS chain."
232
238
  fi
233
239
  done
234
240
  else
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Run the ABC benchmark — reference URL → design-blind brief → render arms A/B/C → blind judge → scorecard with win-rates over n runs
3
3
  argument-hint: [<brief-name>|all] [--n <runs>] [--corpus-version <ver>]
4
+ install: internal
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Render a design from its FORGE:DESIGN_SPEC, critique the rendered pixels against the perceptual negatives, and iterate until it passes or the budget is spent — the missing reward signal.
3
3
  argument-hint: [<issue-number>|<spec-path>] [--max-iters <n>] [--viewports desktop,mobile]
4
+ install: internal
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Route a design GitHub issue through the UI Taste Harness — investigate → architect → implement → critique loop → user-feedback loop → close — accumulating FORGE:DESIGN_* annotations and driving the design:* label state machine to design:shipped.
3
3
  argument-hint: <issue-number> | <brief-name>
4
+ install: internal
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Track command sizes, detect bloat, compare against baselines and thresholds
3
3
  argument-hint: [diff | baseline | full]
4
+ install: internal
4
5
  ---
5
6
  <!-- SPDX-FileCopyrightText: Copyright (c) RapierCraft Studios -->
6
7
  <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
@@ -129,7 +130,7 @@ for cmd in commands/*.md; do
129
130
  done
130
131
 
131
132
  git add .baselines
132
- git commit -m "stats: update command baselines — $(date +%Y-%m-%d)"
133
+ git commit -s -m "stats: update command baselines — $(date +%Y-%m-%d)"
133
134
  ```
134
135
 
135
136
  ---
package/commands/issue.md CHANGED
@@ -495,7 +495,7 @@ Create all {N} issues? (yes / adjust #N / cancel)
495
495
 
496
496
  | Bad Issue | Why It Fails | Good Version |
497
497
  |-----------|-------------|-------------|
498
- | "Fix docker mount" | Which compose file? Which mount? | "fix: `.gemini` volume mount in docker-compose.prod.yml missing rw flag" |
498
+ | "Fix docker mount" | Which compose file? Which mount? | "fix: `logs` volume in docker-compose.yml missing `rw` flag — container writes fail at runtime" |
499
499
  | "Update billing" | What about billing? What's broken? | "fix: payment_processor.charge() raises ZeroDivisionError when amount == 0" |
500
500
  | "Investigate performance" | Investigate what? Where? | "investigate: API p95 latency doubled since last deploy — trace hot paths in job processing routers" |
501
501
  | No affected files listed | Investigator guesses wrong files | List every file with what needs to change |
@@ -382,7 +382,7 @@ Then:
382
382
  --- Next steps ---
383
383
  1. Review generated scripts in .forgedock/scripts/
384
384
  2. Run each script manually to verify output
385
- 3. If correct: git add .forgedock/scripts/ && git commit -m "feat(scripts): adaptive scripts from /optimize"
385
+ 3. If correct: git add .forgedock/scripts/ && git commit -s -m "feat(scripts): adaptive scripts from /optimize"
386
386
  4. If not: edit scripts directly or update forge.yaml → learned: and re-run /optimize
387
387
  ```
388
388