forgedock 1.0.22 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/README.md +255 -211
  2. package/bin/batch-runner.mjs +339 -0
  3. package/bin/cinema.mjs +320 -0
  4. package/bin/demo.mjs +92 -7
  5. package/bin/engine/phases.mjs +168 -0
  6. package/bin/engine/projector.mjs +32 -0
  7. package/bin/engine/reconcile.mjs +11 -0
  8. package/bin/engine/runlog.mjs +79 -0
  9. package/bin/engine/state.mjs +32 -0
  10. package/bin/engine-cli.mjs +222 -0
  11. package/bin/engine.mjs +138 -0
  12. package/bin/forgedock.mjs +896 -1540
  13. package/bin/hooks/interactive-engine.mjs +509 -0
  14. package/bin/hooks/pre-tool-use.mjs +498 -0
  15. package/bin/hooks/subagent-stop-enforce.mjs +197 -0
  16. package/bin/journey.mjs +841 -0
  17. package/bin/registry.mjs +139 -0
  18. package/bin/report.mjs +447 -0
  19. package/bin/runner.mjs +136 -20
  20. package/bin/settings-hook.mjs +346 -0
  21. package/bin/tui.mjs +6 -3
  22. package/commands/adopt.md +4 -1
  23. package/commands/analytics.md +4 -1
  24. package/commands/audit-agents.md +110 -27
  25. package/commands/audit.md +20 -1
  26. package/commands/autopilot.md +84 -12
  27. package/commands/changelog.md +2 -1
  28. package/commands/ci-audit.md +2 -1
  29. package/commands/cleanup.md +161 -1
  30. package/commands/compat-audit.md +2 -1
  31. package/commands/deploy-info.md +63 -4
  32. package/commands/design-bench.md +6 -1
  33. package/commands/design-render-critique-loop.md +2 -1
  34. package/commands/design.md +1 -0
  35. package/commands/diagnose.md +2 -1
  36. package/commands/explain.md +2 -1
  37. package/commands/forge-stats.md +122 -2
  38. package/commands/forgedock-init.md +1 -1
  39. package/commands/geo-audit.md +3 -0
  40. package/commands/incident-response.md +2 -1
  41. package/commands/issue.md +24 -3
  42. package/commands/milestone.md +77 -12
  43. package/commands/optimize.md +2 -1
  44. package/commands/orchestrate/config.md +67 -0
  45. package/commands/orchestrate/phase-1-resolve.md +159 -0
  46. package/commands/orchestrate/phase-2-triage.md +126 -0
  47. package/commands/orchestrate/phase-2.5-synthesis.md +156 -0
  48. package/commands/orchestrate/phase-3-dependency.md +591 -0
  49. package/commands/orchestrate/phase-4-execution.md +962 -0
  50. package/commands/orchestrate/phase-5-cleanup.md +47 -0
  51. package/commands/orchestrate/phase-6-report.md +198 -0
  52. package/commands/orchestrate/safety.md +62 -0
  53. package/commands/orchestrate.md +25 -2027
  54. package/commands/pipeline-health/config.md +34 -0
  55. package/commands/pipeline-health/phase-1-context.md +180 -0
  56. package/commands/pipeline-health/phase-2-metrics.md +1729 -0
  57. package/commands/pipeline-health/phase-3-analyze.md +204 -0
  58. package/commands/pipeline-health/phase-4-proposals.md +117 -0
  59. package/commands/pipeline-health/phase-5-report.md +284 -0
  60. package/commands/pipeline-health/phase-6-summary.md +52 -0
  61. package/commands/pipeline-health.md +25 -2401
  62. package/commands/pipeline-resume.md +24 -1
  63. package/commands/pipeline-status.md +65 -0
  64. package/commands/qa-sweep.md +4 -1
  65. package/commands/quality-gate.md +326 -42
  66. package/commands/replay.md +2 -1
  67. package/commands/review-pr-agents/api.md +135 -0
  68. package/commands/review-pr-agents/auth.md +137 -0
  69. package/commands/review-pr-agents/billing.md +101 -0
  70. package/commands/review-pr-agents/concurrency.md +145 -0
  71. package/commands/review-pr-agents/database.md +103 -0
  72. package/commands/review-pr-agents/frontend.md +89 -0
  73. package/commands/review-pr-agents/infra.md +385 -0
  74. package/commands/review-pr-agents/protocols.md +185 -0
  75. package/commands/review-pr-agents/scraper.md +185 -0
  76. package/commands/review-pr-agents/security.md +406 -0
  77. package/commands/review-pr-agents.md +45 -1722
  78. package/commands/review-pr-staging.md +196 -40
  79. package/commands/review-pr.md +612 -56
  80. package/commands/rollback.md +2 -1
  81. package/commands/scope.md +2 -1
  82. package/commands/security-audit.md +2 -1
  83. package/commands/signal-planner.md +623 -0
  84. package/commands/sync-ecosystem.md +1 -0
  85. package/commands/test-gate.md +17 -1
  86. package/commands/validate.md +2 -1
  87. package/commands/work-on/build/architect.md +28 -0
  88. package/commands/work-on/build/context.md +2 -0
  89. package/commands/work-on/build/implement.md +19 -5
  90. package/commands/work-on/build/validate.md +62 -6
  91. package/commands/work-on/build.md +187 -30
  92. package/commands/work-on/close.md +132 -5
  93. package/commands/work-on/decompose.md +19 -1
  94. package/commands/work-on/investigate.md +117 -2
  95. package/commands/work-on/review.md +48 -4
  96. package/commands/work-on-monolithic.md +8 -2
  97. package/commands/work-on.md +247 -7
  98. package/docs/BRAND.md +16 -5
  99. package/docs/CI.md +2 -2
  100. package/docs/CODEX.md +36 -1
  101. package/docs/CONFIG.md +44 -42
  102. package/docs/FORGE-PROTOCOL.md +15 -1064
  103. package/docs/anthropic-devrel-outreach.md +2 -2
  104. package/docs/articles/command-decomposition-token-savings.md +214 -0
  105. package/docs/articles/model-release-playbook.md +131 -0
  106. package/docs/articles/per-repo-adaptive-scripts-token-savings.md +27 -25
  107. package/docs/claude-breakpoints.json +75 -5
  108. package/docs/design/abc-benchmark.md +1 -1
  109. package/docs/design/design-spec-schema.md +1 -1
  110. package/docs/design/divergent-generation.md +1 -1
  111. package/docs/design/eval-gate-runbook.md +178 -0
  112. package/docs/design/show-hn-draft.md +26 -1
  113. package/docs/design/token-cost-baseline-1298.md +59 -0
  114. package/docs/eval/README.md +117 -0
  115. package/docs/eval/corpus/seed-v1.json +6 -0
  116. package/docs/eval/published/claude-sonnet-4-6-2026-07-04.json +33 -0
  117. package/docs/site/annotations-explained.md +24 -0
  118. package/docs/site/command-learning-path.md +51 -14
  119. package/docs/site/command-reference.md +339 -113
  120. package/docs/site/for-companies.md +95 -0
  121. package/docs/site/forge-annotation-protocol.md +12 -256
  122. package/docs/site/getting-started.md +58 -12
  123. package/docs/site/github-is-the-memory.md +113 -0
  124. package/docs/site/how-it-works.md +3 -13
  125. package/docs/site/index.md +10 -2
  126. package/docs/spec/eval-run-result.md +147 -0
  127. package/docs/spec/forge-protocol-v1.md +441 -18
  128. package/docs/spec/label-state-machine.md +55 -0
  129. package/docs/spec/review-protocol.md +158 -0
  130. package/docs/spec-graph-schema.md +28 -0
  131. package/docs/superpowers/plans/2026-07-03-cinematic-onboarding-tui.md +2339 -0
  132. package/docs/superpowers/plans/2026-07-03-durable-execution-engine.md +1223 -0
  133. package/docs/superpowers/specs/2026-07-03-cinematic-onboarding-tui-design.md +157 -0
  134. package/docs/superpowers/specs/2026-07-03-durable-execution-engine-design.md +194 -0
  135. package/docs/superpowers/specs/2026-07-04-economic-self-governance-design.md +181 -0
  136. package/docs/superpowers/specs/2026-07-04-provenance-trail-design.md +189 -0
  137. package/examples/forgedock-demo/README.md +41 -22
  138. package/examples/forgedock-demo/issues/06-bug-tags-type-validation.md +43 -0
  139. package/examples/forgedock-demo/issues/07-bug-negative-offset-pagination.md +42 -0
  140. package/examples/forgedock-demo/issues/08-bug-patch-mass-assignment.md +49 -0
  141. package/examples/forgedock-demo/issues/09-bug-count-injection.md +47 -0
  142. package/examples/forgedock-demo/issues/10-feature-sort-param.md +39 -0
  143. package/examples/forgedock-demo/issues/11-feature-patch-tags.md +37 -0
  144. package/examples/forgedock-demo/issues/12-feature-bulk-create.md +42 -0
  145. package/examples/forgedock-demo/issues/13-feature-archive-notes.md +46 -0
  146. package/examples/forgedock-demo/issues/14-refactor-extract-title-validation.md +41 -0
  147. package/examples/forgedock-demo/issues/15-refactor-protected-route-flag.md +43 -0
  148. package/examples/forgedock-demo/issues/16-refactor-extract-store.md +45 -0
  149. package/examples/forgedock-demo/issues/17-perf-tags-dedup-set.md +35 -0
  150. package/examples/forgedock-demo/issues/18-perf-remove-redundant-sort.md +37 -0
  151. package/examples/forgedock-demo/issues/19-docs-api-reference-full.md +53 -0
  152. package/examples/forgedock-demo/issues/20-docs-intentional-flaw-convention.md +46 -0
  153. package/examples/forgedock-demo/scripts/smoke.js +18 -1
  154. package/examples/forgedock-demo/src/db.js +36 -11
  155. package/examples/forgedock-demo/src/routes/notes.js +106 -9
  156. package/examples/forgedock-demo/src/server.js +4 -1
  157. package/package.json +12 -4
  158. package/packages/protocol/LICENSE +21 -0
  159. package/packages/protocol/src/cli.js +135 -0
  160. package/packages/protocol/src/emit.js +146 -0
  161. package/packages/protocol/src/index.js +43 -0
  162. package/packages/protocol/src/parse.js +229 -0
  163. package/packages/protocol/src/types.js +171 -0
  164. package/packages/protocol/src/validate.js +122 -0
  165. package/scripts/bench-topology-cost.mjs +372 -0
  166. package/scripts/check-command-docs-drift.sh +198 -0
  167. package/scripts/check-protocol-restatements.sh +176 -0
  168. package/scripts/check-registry/README.md +72 -0
  169. package/scripts/check-registry/manifest.json +6 -0
  170. package/scripts/code-index.sh +454 -0
  171. package/scripts/conformance-check.mjs +168 -0
  172. package/scripts/doctor-pipeline-state.sh +484 -0
  173. package/scripts/eval-gate-baseline.json +17 -0
  174. package/scripts/eval-gate-scorecard.mjs +242 -0
  175. package/scripts/eval-scorecard-aggregator.mjs +252 -0
  176. package/scripts/flaky-quarantine.sh +241 -0
  177. package/scripts/forge-annotation.sh +567 -0
  178. package/scripts/issue-dedup.sh +236 -0
  179. package/scripts/validate-annotation-node.mjs +233 -0
  180. package/scripts/verify-env-vars.sh +18 -2
  181. package/scripts/verify-host-headers.sh +4 -4
  182. package/scripts/verify-route-registration.sh +2 -2
  183. package/scripts/worktree-lifecycle.sh +202 -0
  184. package/templates/devdocs/agent/using-forgedock.md +14 -0
@@ -0,0 +1,841 @@
1
+ /**
2
+ * bin/journey.mjs — The five-act onboarding journey for ForgeDock.
3
+ *
4
+ * Acts (added across Tasks 4–8):
5
+ * preflight() → forge() → read() → review() → celebrate()
6
+ *
7
+ * This file owns forge.yaml generation (single source of truth, driven from
8
+ * detection values) and orchestrates the existing modules:
9
+ * init-detect.mjs, init-enrich-api.mjs, tui.annotatedReviewScreen, registry.mjs
10
+ */
11
+
12
+ import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from "fs";
13
+ import { join, basename } from "path";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // forge.yaml generation (Task 4)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const esc = (s) => String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n");
20
+
21
+ /** Append the TODO flag for a low-confidence field. */
22
+ function todo(key, low) {
23
+ return low.includes(key) ? ` # TODO(forgedock:${key}) — verify this value` : "";
24
+ }
25
+
26
+ /**
27
+ * Write forge.yaml from reviewed values.
28
+ * @param {{owner:string,repo:string,name:string,description:string,root:string,
29
+ * worktreeBase:string,defaultBranch:string,stagingBranch:string}} v
30
+ * Exactly the object annotatedReviewScreen resolves with.
31
+ * @param {string[]} lowConfidenceKeys - keys to flag with # TODO comments.
32
+ * @param {string} outputPath
33
+ * @returns {{ todoCount: number }}
34
+ */
35
+ export function writeForgeYaml(v, lowConfidenceKeys, outputPath) {
36
+ const low = lowConfidenceKeys;
37
+ const content = `# forge.yaml — ForgeDock Configuration
38
+ #
39
+ # Auto-generated by: npx forgedock
40
+ # Fields marked with TODO comments below were guessed — verify them.
41
+ #
42
+ # Required sections: project, paths, branches
43
+ # Optional sections: repos, project_board, services, review, verification
44
+ #
45
+ # See docs/CONFIG.md for full reference.
46
+
47
+ # =============================================================================
48
+ # PROJECT (REQUIRED)
49
+ # =============================================================================
50
+
51
+ project:
52
+ name: "${esc(v.name)}"${todo("name", low)}
53
+ owner: "${esc(v.owner)}"${todo("owner", low)}
54
+ repo: "${esc(v.repo)}"${todo("repo", low)}
55
+ description: "${esc(v.description)}"${todo("description", low)}
56
+
57
+ # =============================================================================
58
+ # PATHS (REQUIRED)
59
+ # =============================================================================
60
+
61
+ paths:
62
+ root: "${esc(v.root)}"${todo("root", low)}
63
+ worktree_base: "${esc(v.worktreeBase)}"${todo("worktreeBase", low)}
64
+
65
+ # =============================================================================
66
+ # BRANCHES (REQUIRED)
67
+ # =============================================================================
68
+
69
+ branches:
70
+ default: "${esc(v.defaultBranch)}"${todo("defaultBranch", low)}
71
+ staging: "${esc(v.stagingBranch)}"${todo("stagingBranch", low)}
72
+ feature_pattern: "milestone/{slug}"
73
+
74
+ # =============================================================================
75
+ # REPOS (OPTIONAL) — multi-repo configuration. Remove the # to enable.
76
+ # =============================================================================
77
+
78
+ # repos:
79
+ # default:
80
+ # repo: "${esc(v.owner)}/${esc(v.repo)}"
81
+ # staging_branch: "${esc(v.stagingBranch)}"
82
+ # satellites:
83
+ # - prefix: "mcp"
84
+ # repo: "${esc(v.owner)}/your-satellite-repo"
85
+ # staging_branch: "main"
86
+
87
+ # =============================================================================
88
+ # PROJECT BOARD (OPTIONAL) — GitHub Projects v2 integration.
89
+ # To find IDs: gh project list --owner ${esc(v.owner)}
90
+ # =============================================================================
91
+
92
+ # project_board:
93
+ # owner: "${esc(v.owner)}"
94
+ # project_number: 1
95
+ # project_id: "PVT_kwHOxxxxxxxxxxxxxxxx"
96
+ # field_ids:
97
+ # status: "PVTSSF_xxxxxxxxxxxxxxxxxxxxxxxx"
98
+
99
+ # =============================================================================
100
+ # REVIEW (OPTIONAL) — context injected into review agent prompts.
101
+ # =============================================================================
102
+
103
+ # review:
104
+ # tech_stack: "Node.js, TypeScript, PostgreSQL"
105
+ # context: |
106
+ # Describe your repo structure and any unusual conventions here.
107
+
108
+ # =============================================================================
109
+ # VERIFICATION (OPTIONAL) — health checks for quality gate / validate.
110
+ # =============================================================================
111
+
112
+ # verification:
113
+ # health_endpoint: "https://api.example.com/health"
114
+ # health_patterns:
115
+ # - '"status": "ok"'
116
+ `;
117
+ // Atomic write: write to a temp file first, then rename into place.
118
+ // If the write fails (e.g. ENOSPC), the original file is untouched and
119
+ // any partial .tmp is cleaned up — no corrupt or missing forge.yaml.
120
+ const tmpPath = outputPath + ".tmp";
121
+ try {
122
+ writeFileSync(tmpPath, content, "utf-8");
123
+ renameSync(tmpPath, outputPath);
124
+ } catch (err) {
125
+ try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
126
+ throw err;
127
+ }
128
+ const todoCount = (content.match(/# TODO\(forgedock:/g) || []).length;
129
+ return { todoCount };
130
+ }
131
+
132
+ /** Maps manual-flow value keys to their location in a ConfigDraft. */
133
+ const DRAFT_KEY_PATHS = {
134
+ owner: ["project", "owner"],
135
+ repo: ["project", "repo"],
136
+ name: ["project", "name"],
137
+ root: ["paths", "root"],
138
+ worktreeBase: ["paths", "worktreeBase"],
139
+ defaultBranch: ["branches", "default"],
140
+ stagingBranch: ["branches", "staging"],
141
+ };
142
+
143
+ /**
144
+ * Compute which fields accepted in the `--manual` init flow should be flagged
145
+ * low-confidence (and so get a `# TODO(forgedock:<field>)` comment).
146
+ *
147
+ * A field counts as low-confidence when detection scored it "low" AND the
148
+ * user accepted the detected value unchanged (didn't type over the default).
149
+ * `description` is special-cased: it has no draft entry, so it counts as low
150
+ * when detection found nothing (empty) and the user also left it empty.
151
+ *
152
+ * Pure — no I/O, safe to unit test directly.
153
+ *
154
+ * @param {import('./init-detect.mjs').ConfigDraft} draft
155
+ * @param {{ value: string, source: string }} description
156
+ * @param {{owner:string,repo:string,name:string,description:string,root:string,
157
+ * worktreeBase:string,defaultBranch:string,stagingBranch:string}} values
158
+ * The values accepted from the manual prompts.
159
+ * @returns {string[]}
160
+ */
161
+ export function manualLowConfidenceKeys(draft, description, values) {
162
+ const keys = [];
163
+ for (const [key, path] of Object.entries(DRAFT_KEY_PATHS)) {
164
+ const field = path.reduce(
165
+ (node, k) => (node && typeof node === "object" ? node[k] : undefined),
166
+ draft,
167
+ );
168
+ if (field && field.confidence === "low" && values[key] === field.value) {
169
+ keys.push(key);
170
+ }
171
+ }
172
+ if (!description.value && !values.description) {
173
+ keys.push("description");
174
+ }
175
+ return keys;
176
+ }
177
+
178
+ /**
179
+ * Back up an existing file to <name>.bak (timestamped if .bak exists).
180
+ * @returns {{ backupName: string } | null} null when the file didn't exist.
181
+ */
182
+ export function backupExisting(outputPath) {
183
+ if (!existsSync(outputPath)) return null;
184
+ const baseBak = outputPath + ".bak";
185
+ const backupPath = existsSync(baseBak)
186
+ ? `${baseBak}.${new Date().toISOString().replace(/[:.]/g, "-")}`
187
+ : baseBak;
188
+ renameSync(outputPath, backupPath);
189
+ return { backupName: basename(backupPath) };
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Description detection (moved from forgedock.mjs init(), Task 4)
194
+ // ---------------------------------------------------------------------------
195
+
196
+ function firstParagraph(content) {
197
+ const lines = content.split("\n");
198
+ let started = false;
199
+ const out = [];
200
+ for (const line of lines) {
201
+ if (!started && line.match(/^#/)) continue;
202
+ if (!started && line.trim() === "") continue;
203
+ if (!started && line.match(/^[!<\[`|]/)) continue;
204
+ if (!started && line.match(/^---/)) continue;
205
+ if (!started) started = true;
206
+ if (line.trim() === "") break;
207
+ out.push(line.trim());
208
+ }
209
+ if (out.length === 0) return "";
210
+ return out
211
+ .join(" ")
212
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
213
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
214
+ .replace(/`([^`]+)`/g, "$1")
215
+ .slice(0, 200)
216
+ .trim();
217
+ }
218
+
219
+ /**
220
+ * Detect a project description from README.md, falling back to CLAUDE.md.
221
+ * @returns {{ value: string, source: 'README.md'|'CLAUDE.md'|'' }}
222
+ */
223
+ export function detectDescription(cwd) {
224
+ for (const file of ["README.md", "CLAUDE.md"]) {
225
+ try {
226
+ const p = join(cwd, file);
227
+ if (!existsSync(p)) continue;
228
+ const value = firstParagraph(readFileSync(p, "utf-8").slice(0, 2048));
229
+ if (value) return { value, source: file };
230
+ } catch {
231
+ // best-effort only
232
+ }
233
+ }
234
+ return { value: "", source: "" };
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // Journey context (Task 5)
239
+ // ---------------------------------------------------------------------------
240
+
241
+ import { execFileSync } from "child_process";
242
+ import os from "os";
243
+ import {
244
+ renderMark, ember, shimmer, revealRows, moltenBar, fixCard,
245
+ colorMode, motionEnabled, CHROME_STOPS, HERO_MARK, COMPACT_MARK, sleep,
246
+ } from "./cinema.mjs";
247
+
248
+ /**
249
+ * Build the shared journey context. Every act takes this as its first arg.
250
+ * All process-touching values are injectable for tests.
251
+ */
252
+ export function makeCtx(overrides = {}) {
253
+ const env = overrides.env ?? process.env;
254
+ const stdout = overrides.stdout ?? process.stdout;
255
+ const argv = overrides.argv ?? process.argv.slice(2);
256
+ const nodeVersion = overrides.nodeVersion ?? process.versions.node;
257
+ const enrichFn = overrides.enrichFn ?? enrich;
258
+ return {
259
+ cwd: process.cwd(),
260
+ home: env.HOME || env.USERPROFILE || os.homedir(),
261
+ forgeHome: "",
262
+ argv,
263
+ env,
264
+ stdout,
265
+ mode: colorMode(env, stdout),
266
+ motion: motionEnabled(argv, env, stdout),
267
+ nodeVersion,
268
+ linkStrategy: "symlink",
269
+ enrichFn,
270
+ exec: (cmd, args) =>
271
+ execFileSync(cmd, args, {
272
+ encoding: "utf-8",
273
+ stdio: ["pipe", "pipe", "pipe"],
274
+ timeout: 10000,
275
+ }).trim(),
276
+ startedAt: Date.now(),
277
+ ...overrides,
278
+ };
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Act I — Ignition: hero mark + preflight checks (Task 5)
283
+ // ---------------------------------------------------------------------------
284
+
285
+ const dimLine = (ctx, s) => (ctx.mode === "none" ? s : `\x1b[2m${s}\x1b[22m`);
286
+
287
+ /**
288
+ * Render the hero banner and run preflight checks. Failures render fix cards
289
+ * and the journey continues — advisory, never fatal.
290
+ * @returns {Promise<{ checks: Array<{name, ok, detail, fix?}>, ghReady: boolean }>}
291
+ */
292
+ export async function preflight(ctx) {
293
+ const { stdout: w } = ctx;
294
+ w.write("\n");
295
+ await shimmer(HERO_MARK, CHROME_STOPS, { mode: ctx.mode, motion: ctx.motion, writer: w });
296
+ w.write("\n " + ember("F O R G E D O C K", ctx.mode) + "\n");
297
+ w.write(" " + dimLine(ctx, "──── lighting the forge ────────────────────") + "\n\n");
298
+
299
+ const rows = [
300
+ {
301
+ label: "Node",
302
+ run: async () => {
303
+ const major = Number(ctx.nodeVersion.split(".")[0]);
304
+ return major >= 18
305
+ ? { ok: true, detail: `v${ctx.nodeVersion}` }
306
+ : { ok: false, detail: `v${ctx.nodeVersion} — need ≥18`, fix: ["Upgrade Node: https://nodejs.org/"] };
307
+ },
308
+ },
309
+ {
310
+ label: "git",
311
+ run: async () => {
312
+ try {
313
+ const v = ctx.exec("git", ["--version"]);
314
+ return { ok: true, detail: v.replace(/^git version\s*/, "") };
315
+ } catch {
316
+ return { ok: false, detail: "not found", fix: ["Install git: https://git-scm.com/downloads"] };
317
+ }
318
+ },
319
+ },
320
+ {
321
+ label: "Claude Code",
322
+ run: async () => {
323
+ const claudeDir = join(ctx.home, ".claude");
324
+ return existsSync(claudeDir)
325
+ ? { ok: true, detail: "~/.claude found" }
326
+ : { ok: false, detail: "~/.claude not found", fix: ["Install Claude Code: https://claude.com/claude-code"] };
327
+ },
328
+ },
329
+ {
330
+ label: "GitHub CLI",
331
+ run: async () => {
332
+ try {
333
+ ctx.exec("gh", ["--version"]);
334
+ } catch {
335
+ return {
336
+ ok: false,
337
+ detail: "not found",
338
+ fix: ["Install gh: https://cli.github.com/", "Windows: winget install GitHub.cli"],
339
+ };
340
+ }
341
+ try {
342
+ ctx.exec("gh", ["auth", "status"]);
343
+ return { ok: true, detail: "authenticated" };
344
+ } catch {
345
+ return { ok: false, detail: "not authenticated", fix: ["Run: gh auth login"] };
346
+ }
347
+ },
348
+ },
349
+ ];
350
+
351
+ const results = await revealRows(rows, { mode: ctx.mode, motion: ctx.motion, writer: w });
352
+ const named = rows.map((r, i) => ({ name: r.label, ...results[i] }));
353
+ return { checks: named, ghReady: named[3].ok };
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Act II — Forging: command symlinks + SessionStart hook (Task 6)
358
+ // ---------------------------------------------------------------------------
359
+
360
+ import { mkdir, symlink, readlink, lstat, readdir, rename, copyFile, readFile, writeFile, unlink } from "fs/promises";
361
+ import { relative, dirname as pathDirname } from "path";
362
+ import {
363
+ installSessionStartHook,
364
+ installSubagentStopHook,
365
+ installPreToolUseHook,
366
+ removeSubagentStopEnforceHook,
367
+ } from "./settings-hook.mjs";
368
+
369
+ /**
370
+ * Parse the `install:` tier from a command spec's YAML frontmatter block.
371
+ *
372
+ * Reads the first 512 bytes of the file synchronously (large enough to reach
373
+ * any realistic frontmatter block without loading the full spec into memory).
374
+ * Strips a leading UTF-8 BOM if present — BOM-prefixed files would cause the
375
+ * frontmatter regex to miss the leading `---` delimiter.
376
+ *
377
+ * Tier values:
378
+ * 'core' — install for all users (default when key is absent or on any error)
379
+ * 'extras' — opt-in, not installed by default; use `npx forgedock install --extras` (#1257)
380
+ * 'internal' — ForgeDock development only; never installed to user machines
381
+ *
382
+ * Fail-open: any read error, malformed frontmatter, or unrecognised value falls
383
+ * back to `'core'` so the command is installed rather than silently excluded.
384
+ *
385
+ * @param {string} filePath - Absolute path to the .md command spec.
386
+ * @returns {'core' | 'extras' | 'internal'}
387
+ */
388
+ export function parseInstallTier(filePath) {
389
+ try {
390
+ // Read the file as UTF-8. readFileSync is already imported from "fs" at the
391
+ // top of this module — no require() needed in this ESM file. For large specs
392
+ // only the first 512 bytes are relevant (frontmatter fits comfortably within
393
+ // that budget), but readFileSync is simpler and safe for the file sizes here.
394
+ let content = readFileSync(filePath, "utf-8");
395
+ // Strip BOM — a BOM-prefixed file would shift the leading `---` off the
396
+ // start of the string and cause the frontmatter regex to miss it entirely.
397
+ // (Ref: review-finding #657 — parseFrontmatter silently falls back on BOM files)
398
+ content = content.replace(/^\uFEFF/, "");
399
+
400
+ // Match YAML frontmatter block: must start at position 0.
401
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
402
+ if (!match) return "core";
403
+
404
+ const raw = match[1];
405
+ for (const line of raw.split(/\r?\n/)) {
406
+ const colonIdx = line.indexOf(":");
407
+ if (colonIdx === -1) continue;
408
+ const key = line.slice(0, colonIdx).trim();
409
+ if (key !== "install") continue;
410
+ const val = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, "");
411
+ if (val === "internal" || val === "extras" || val === "core") return val;
412
+ return "core"; // unrecognised value → default
413
+ }
414
+ return "core"; // key absent
415
+ } catch {
416
+ return "core"; // any read/parse error → fail-open
417
+ }
418
+ }
419
+
420
+ /** Recursively find installable .md files, sorted (moved from forgedock.mjs).
421
+ *
422
+ * Files with `install: internal` in their YAML frontmatter are excluded — they
423
+ * are ForgeDock-development-only specs that must never reach user machines.
424
+ * Files with `install: core` (or no `install:` key) are included. Files with
425
+ * `install: extras` are excluded from the default install surface; they are
426
+ * available via `npx forgedock install --extras` (implemented in #1257).
427
+ *
428
+ * @param {string} dir - Directory to search recursively.
429
+ * @param {{ includeExtras?: boolean }} [opts] - Options.
430
+ * includeExtras: when true, also include `install: extras` specs (opt-in tier).
431
+ */
432
+ export async function findMarkdownFiles(dir, opts = {}) {
433
+ const { includeExtras = false } = opts;
434
+ const results = [];
435
+ const entries = await readdir(dir, { withFileTypes: true });
436
+ for (const entry of entries) {
437
+ const full = join(dir, entry.name);
438
+ if (entry.isDirectory()) {
439
+ results.push(...(await findMarkdownFiles(full, opts)));
440
+ } else if (entry.name.endsWith(".md")) {
441
+ const tier = parseInstallTier(full);
442
+ if (tier === "core" || (includeExtras && tier === "extras")) {
443
+ results.push(full);
444
+ }
445
+ // 'internal' is always excluded from the install surface
446
+ }
447
+ }
448
+ return results.sort();
449
+ }
450
+
451
+ /** Load the copied-commands ownership manifest (missing/corrupt → empty). */
452
+ async function loadCopiedManifest(manifestPath) {
453
+ try {
454
+ const parsed = JSON.parse(await readFile(manifestPath, "utf-8"));
455
+ if (parsed && typeof parsed === "object" && parsed.files && typeof parsed.files === "object") {
456
+ return { version: 1, files: parsed.files };
457
+ }
458
+ } catch {
459
+ // missing or corrupt → start fresh
460
+ }
461
+ return { version: 1, files: {} };
462
+ }
463
+
464
+ /** Save the manifest atomically (mkdir recursive + .tmp+rename). */
465
+ async function saveCopiedManifest(manifestPath, manifest) {
466
+ await mkdir(pathDirname(manifestPath), { recursive: true });
467
+ await writeFile(manifestPath + ".tmp", JSON.stringify(manifest, null, 2) + "\n", "utf-8");
468
+ await rename(manifestPath + ".tmp", manifestPath);
469
+ }
470
+
471
+ const isLinkPermissionError = (err) => err.code === "EPERM" || err.code === "EACCES";
472
+
473
+ /**
474
+ * Act II: link commands into ~/.claude/commands with a molten progress line,
475
+ * then register the SessionStart hook.
476
+ * Symlink semantics preserved verbatim from the original install():
477
+ * regular files are skipped with a warning; changed links updated atomically.
478
+ * On Windows without Developer Mode (symlink → EPERM/EACCES) each command is
479
+ * copied instead, and recorded in a manifest so re-runs can tell ForgeDock's
480
+ * own copies apart from user-owned files. ctx.linkStrategy ("symlink" default)
481
+ * can be set to "copy" to skip all symlink attempts (deterministic tests).
482
+ */
483
+ export async function forge(ctx) {
484
+ const { stdout: w } = ctx;
485
+ const commandsDir = join(ctx.forgeHome, "commands");
486
+ const targetDir = join(ctx.home, ".claude", "commands");
487
+ const manifestPath = join(ctx.home, ".claude", "forgedock", "copied-commands.json");
488
+ const wantSymlink = ctx.linkStrategy !== "copy";
489
+
490
+ w.write("\n " + ember("Forging commands", ctx.mode) + " " + dimLine(ctx, `into ${targetDir}`) + "\n\n");
491
+ await mkdir(targetDir, { recursive: true });
492
+
493
+ const manifest = await loadCopiedManifest(manifestPath);
494
+ let manifestChanged = false;
495
+
496
+ const files = await findMarkdownFiles(commandsDir, { includeExtras: !!ctx.includeExtras });
497
+ let installed = 0, updated = 0, skipped = 0, copied = 0;
498
+ const barWidth = 24;
499
+ let barShown = false;
500
+
501
+ const recordCopy = (rel) => {
502
+ if (!manifest.files[rel]) {
503
+ manifest.files[rel] = true;
504
+ manifestChanged = true;
505
+ }
506
+ };
507
+
508
+ for (let i = 0; i < files.length; i++) {
509
+ const file = files[i];
510
+ const rel = relative(commandsDir, file);
511
+ const target = join(targetDir, rel);
512
+ await mkdir(pathDirname(target), { recursive: true });
513
+
514
+ try {
515
+ const stats = await lstat(target);
516
+ if (stats.isSymbolicLink()) {
517
+ const current = await readlink(target);
518
+ if (current === file) {
519
+ skipped++;
520
+ } else {
521
+ let relinked = false;
522
+ if (wantSymlink) {
523
+ try {
524
+ await symlink(file, target + ".tmp");
525
+ try {
526
+ await rename(target + ".tmp", target);
527
+ } catch (renameErr) {
528
+ await unlink(target + ".tmp").catch(() => {});
529
+ throw renameErr;
530
+ }
531
+ updated++;
532
+ relinked = true;
533
+ } catch (linkErr) {
534
+ if (!isLinkPermissionError(linkErr)) throw linkErr;
535
+ }
536
+ }
537
+ if (!relinked) {
538
+ // Can't (or shouldn't) re-link — replace the managed link with a copy.
539
+ // unlink first: copyFile onto a symlink writes THROUGH the link.
540
+ await unlink(target);
541
+ await copyFile(file, target);
542
+ updated++; // replaces an existing managed entry
543
+ recordCopy(rel);
544
+ }
545
+ }
546
+ } else if (manifest.files[rel]) {
547
+ // A regular file we copied on a previous run — ours to manage.
548
+ // First try upgrading it to a symlink (Developer Mode enabled since).
549
+ let upgraded = false;
550
+ if (wantSymlink) {
551
+ try {
552
+ await symlink(file, target + ".tmp");
553
+ try {
554
+ await rename(target + ".tmp", target); // rename replaces existing files on Windows
555
+ } catch (renameErr) {
556
+ await unlink(target + ".tmp").catch(() => {});
557
+ throw renameErr;
558
+ }
559
+ updated++;
560
+ delete manifest.files[rel];
561
+ manifestChanged = true;
562
+ upgraded = true;
563
+ } catch (linkErr) {
564
+ if (!isLinkPermissionError(linkErr)) throw linkErr;
565
+ }
566
+ }
567
+ if (!upgraded) {
568
+ const [src, dst] = await Promise.all([readFile(file), readFile(target)]);
569
+ if (src.equals(dst)) {
570
+ skipped++;
571
+ } else {
572
+ await copyFile(file, target);
573
+ updated++;
574
+ }
575
+ }
576
+ } else {
577
+ if (barShown) { w.write("\x1b[1A\x1b[2K"); barShown = false; }
578
+ w.write(` WARNING: ${rel} is a regular file — skipping (remove it manually to let ForgeDock manage it)\n`);
579
+ skipped++;
580
+ }
581
+ } catch (err) {
582
+ if (err.code !== "ENOENT") throw err;
583
+ let linked = false;
584
+ if (wantSymlink) {
585
+ try {
586
+ await symlink(file, target);
587
+ installed++;
588
+ linked = true;
589
+ if (manifest.files[rel]) {
590
+ // Symlinks work now and the old copy is gone — drop the stale record.
591
+ delete manifest.files[rel];
592
+ manifestChanged = true;
593
+ }
594
+ } catch (linkErr) {
595
+ if (!isLinkPermissionError(linkErr)) throw linkErr;
596
+ }
597
+ }
598
+ if (!linked) {
599
+ // Windows without Developer Mode/admin — fall back to a copy.
600
+ await copyFile(file, target);
601
+ copied++;
602
+ recordCopy(rel);
603
+ }
604
+ }
605
+
606
+ if (ctx.motion) {
607
+ if (barShown) w.write("\x1b[1A\x1b[2K");
608
+ w.write(` ${moltenBar(i + 1, files.length, { width: barWidth, mode: ctx.mode })} ${i + 1}/${files.length} ${dimLine(ctx, "/" + rel.replace(/\.md$/, ""))}\n`);
609
+ barShown = true;
610
+ }
611
+ }
612
+ if (barShown) w.write("\x1b[1A\x1b[2K");
613
+
614
+ const hookScript = join(ctx.forgeHome, "bin", "hooks", "session-start.mjs");
615
+ const settingsPath = join(ctx.home, ".claude", "settings.json");
616
+ const { status: hookStatus } = installSessionStartHook(settingsPath, hookScript);
617
+
618
+ // Install enforcement hooks (#1250): PreToolUse (branch/label validation)
619
+ // and SubagentStop interactive engine adapter. Both are idempotent and
620
+ // always installed (fail-open if settings.json is malformed, same
621
+ // contract as SessionStart hook).
622
+ const preToolUseScript = join(ctx.forgeHome, "bin", "hooks", "pre-tool-use.mjs");
623
+ const subagentStopScript = join(ctx.forgeHome, "bin", "hooks", "interactive-engine.mjs");
624
+ const { status: preToolUseStatus } = installPreToolUseHook(settingsPath, preToolUseScript);
625
+ installSubagentStopHook(settingsPath, subagentStopScript);
626
+
627
+ // SubagentStop annotation-verifier hook (#1250) is NOT installed: its
628
+ // trigger condition (a `FORGE:PHASE_START` marker in the transcript) is
629
+ // never emitted anywhere in the pipeline, so it always exits 0 with zero
630
+ // enforcement effect while still spawning a process + reading the
631
+ // transcript on every SubagentStop (forge#1527). Actively clean up any
632
+ // prior installation instead of installing it.
633
+ const { status: subagentStopEnforceStatus } = removeSubagentStopEnforceHook(settingsPath);
634
+
635
+ // Housekeeping — must never abort the receipt or the hook.
636
+ let manifestSaveFailed = false;
637
+ if (manifestChanged) {
638
+ try {
639
+ await saveCopiedManifest(manifestPath, manifest);
640
+ } catch {
641
+ manifestSaveFailed = true;
642
+ }
643
+ }
644
+
645
+ const glyph = (ok) => (ctx.mode === "none" ? (ok ? "✔" : "!") : `\x1b[38;2;255;179;71m${ok ? "✔" : "!"}\x1b[0m`);
646
+ const headlineVerb = copied > 0 ? "installed" : "linked";
647
+ const headlineDetail = copied > 0
648
+ ? `(new ${installed}, copied ${copied}, updated ${updated}, unchanged ${skipped})`
649
+ : `(new ${installed}, updated ${updated}, unchanged ${skipped})`;
650
+ w.write(` ${glyph(true)} ${files.length} slash commands ${headlineVerb} ${dimLine(ctx, headlineDetail)}\n`);
651
+ if (copied > 0) {
652
+ w.write(` ${glyph(false)} ${copied} copied (not linked) ${dimLine(ctx, "— enable Windows Developer Mode for live-updating links")}\n`);
653
+ }
654
+ if (manifestSaveFailed) {
655
+ w.write(" " + dimLine(ctx, "manifest not saved — re-runs may warn about copied files") + "\n");
656
+ }
657
+ if (hookStatus === "skipped-malformed") {
658
+ w.write(` ${glyph(false)} SessionStart hook NOT registered — ${settingsPath} is not valid JSON\n`);
659
+ w.write(fixCard([`Fix the JSON in ${settingsPath}, then re-run: npx forgedock install`], ctx.mode) + "\n");
660
+ } else {
661
+ w.write(` ${glyph(true)} SessionStart hook ${hookStatus === "already" ? "active" : "registered"} ${dimLine(ctx, settingsPath)}\n`);
662
+ }
663
+
664
+ // Report enforcement hook status.
665
+ if (preToolUseStatus !== null) {
666
+ w.write(` ${glyph(true)} PreToolUse enforcement hook ${preToolUseStatus === "already" ? "active" : "registered"} ${dimLine(ctx, "(branch/label enforcement)")}\n`);
667
+ } else {
668
+ w.write(" " + dimLine(ctx, "PreToolUse hook skipped — requires Claude Code v2.1.163+") + "\n");
669
+ }
670
+ // "skipped-malformed" and "absent" are intentionally silent here:
671
+ // - skipped-malformed: installSessionStartHook() above reads the same
672
+ // settingsPath and already prints the malformed-JSON fix-card, so a
673
+ // second message would just repeat the same signal.
674
+ // - absent: the expected steady state (nothing to remove) — this
675
+ // function only reports state *changes*, not no-ops.
676
+ if (subagentStopEnforceStatus === "removed") {
677
+ w.write(` ${glyph(true)} SubagentStop enforcement hook removed ${dimLine(ctx, "(non-functional — see forge#1527)")}\n`);
678
+ }
679
+
680
+ return { installed, updated, skipped, copied, total: files.length, hookStatus, preToolUseStatus, subagentStopEnforceStatus };
681
+ }
682
+
683
+ // ---------------------------------------------------------------------------
684
+ // Act III — Reading your repository (Task 8)
685
+ // ---------------------------------------------------------------------------
686
+
687
+ import { detectConfig } from "./init-detect.mjs";
688
+ import { enrich } from "./init-enrich-api.mjs";
689
+ import { annotatedReviewScreen, box } from "./tui.mjs";
690
+
691
+ const badgeOf = (field) => field.confidence;
692
+
693
+ /**
694
+ * Act III: detect the repo, show each field as a reveal row with its
695
+ * confidence badge, optionally AI-enrich. Detection runs first (it is fast);
696
+ * rows are display pacing, not fake latency.
697
+ */
698
+ export async function read(ctx) {
699
+ const { stdout: w } = ctx;
700
+ w.write("\n " + ember("Reading your repository", ctx.mode) + "\n\n");
701
+
702
+ let draft = await detectConfig(ctx.cwd);
703
+ const description = detectDescription(ctx.cwd);
704
+
705
+ const rows = [
706
+ { label: "owner/repo", run: async () => ({ ok: true, detail: `${draft.project.owner.value}/${draft.project.repo.value}`, badge: badgeOf(draft.project.owner) }) },
707
+ { label: "default branch", run: async () => ({ ok: true, detail: draft.branches.default.value, badge: badgeOf(draft.branches.default) }) },
708
+ { label: "staging branch", run: async () => ({ ok: true, detail: draft.branches.staging.value, badge: badgeOf(draft.branches.staging) }) },
709
+ { label: "project name", run: async () => ({ ok: true, detail: draft.project.name.value, badge: badgeOf(draft.project.name) }) },
710
+ {
711
+ label: "description",
712
+ run: async () => description.value
713
+ ? { ok: true, detail: `"${description.value.slice(0, 40)}${description.value.length > 40 ? "…" : ""}"`, badge: "medium" }
714
+ : { ok: true, detail: "none found", badge: "low" },
715
+ },
716
+ ];
717
+ await revealRows(rows, { mode: ctx.mode, motion: ctx.motion, writer: w });
718
+
719
+ if (ctx.env.ANTHROPIC_API_KEY) {
720
+ try {
721
+ w.write(" " + dimLine(ctx, "✦ enriching with AI…") + "\n");
722
+ const enriched = await ctx.enrichFn(draft);
723
+ if (enriched && typeof enriched === "object") draft = enriched;
724
+ } catch {
725
+ w.write(" " + dimLine(ctx, "✦ AI enrichment unavailable — continuing with detection only") + "\n");
726
+ }
727
+ } else {
728
+ w.write(" " + dimLine(ctx, "✦ no ANTHROPIC_API_KEY — skipping AI enrichment") + "\n");
729
+ }
730
+
731
+ return { draft, description };
732
+ }
733
+
734
+ // ---------------------------------------------------------------------------
735
+ // Act IV — The Review (Task 8)
736
+ // ---------------------------------------------------------------------------
737
+
738
+ /**
739
+ * Act IV: the single interaction. Non-TTY + existing config aborts to protect
740
+ * the file; non-TTY + fresh config writes detection values with TODO flags
741
+ * (annotatedReviewScreen's non-TTY path returns them directly).
742
+ */
743
+ export async function review(ctx, draft, description) {
744
+ const { stdout: w } = ctx;
745
+ const outputPath = join(ctx.cwd, "forge.yaml");
746
+ const hasExisting = existsSync(outputPath);
747
+
748
+ if (hasExisting && process.stdin.isTTY !== true) {
749
+ w.write("\n forge.yaml already exists — non-interactive run, aborting to protect it.\n");
750
+ w.write(" " + dimLine(ctx, "Run interactively (or delete forge.yaml) to regenerate.") + "\n");
751
+ return { written: false, todoCount: 0, backupName: null, aborted: true };
752
+ }
753
+
754
+ const extraFields = description.value
755
+ ? { description: { value: description.value, confidence: "medium", source: description.source, why: `First paragraph of ${description.source}` } }
756
+ : {};
757
+
758
+ const accepted = await annotatedReviewScreen(draft, {
759
+ hasExistingConfig: hasExisting,
760
+ showSources: ctx.argv.includes("--verbose"),
761
+ extraFields,
762
+ });
763
+
764
+ const backup = backupExisting(outputPath);
765
+ if (backup) w.write(` Backed up: forge.yaml → ${backup.backupName}\n`);
766
+
767
+ const { todoCount } = writeForgeYaml(accepted, accepted.lowConfidenceKeys, outputPath);
768
+ return { written: true, todoCount, backupName: backup ? backup.backupName : null, aborted: false };
769
+ }
770
+
771
+ // ---------------------------------------------------------------------------
772
+ // Act V — Forged (Task 8)
773
+ // ---------------------------------------------------------------------------
774
+
775
+ /**
776
+ * Act V: quench flash on the compact mark, receipt with real elapsed time,
777
+ * next-steps box.
778
+ */
779
+ export function celebrate(ctx, summary) {
780
+ const { stdout: w } = ctx;
781
+ const elapsed = Math.round((Date.now() - ctx.startedAt) / 1000);
782
+ const mark = renderMark("compact", ctx.mode);
783
+
784
+ w.write("\n");
785
+ w.write(mark[0] + "\n");
786
+ w.write(mark[1] + " " + ember("Forged.", ctx.mode) + " " + dimLine(ctx, `install → config in ${elapsed}s`) + "\n");
787
+ w.write(mark[2] + "\n");
788
+ w.write(mark[3] + "\n\n");
789
+
790
+ const glyph = ctx.mode === "none" ? "✔" : "\x1b[38;2;255;179;71m✔\x1b[0m";
791
+ if (summary.written) {
792
+ const todoNote = summary.todoCount > 0 ? `${summary.todoCount} field${summary.todoCount === 1 ? "" : "s"} flagged # TODO` : "all fields detected";
793
+ w.write(` ${glyph} forge.yaml written ${dimLine(ctx, todoNote)}\n`);
794
+ }
795
+ if (summary.total !== undefined) {
796
+ const hookNote = summary.hookStatus === "skipped-malformed" ? "hook NOT active — see fix above" : "Claude Code knows this repo";
797
+ w.write(` ${glyph} ${summary.total} commands · hook ${summary.hookStatus === "skipped-malformed" ? "skipped" : "active"} ${dimLine(ctx, hookNote)}\n`);
798
+ }
799
+ w.write("\n");
800
+ if (summary.isMinimal) {
801
+ w.write(" " + dimLine(ctx, "see docs/CONFIG.md for optional sections") + "\n");
802
+ }
803
+ w.write(
804
+ box(
805
+ [
806
+ ` 1. run npx forgedock labels setup — bootstrap GitHub labels`,
807
+ ` 2. run npx forgedock doctor — verify the install is green`,
808
+ ` 3. open claude in this repo`,
809
+ ` 4. run /issue <title> then /work-on <number>`,
810
+ ],
811
+ { title: "what's next" },
812
+ ),
813
+ );
814
+ w.write(" " + dimLine(ctx, "docs: github.com/RapierCraftStudios/ForgeDock · ⭐ a star is the whole marketing budget") + "\n\n");
815
+ }
816
+
817
+ // ---------------------------------------------------------------------------
818
+ // The full journey (Task 8)
819
+ // ---------------------------------------------------------------------------
820
+
821
+ /**
822
+ * Acts I→V. Returns the process exit code.
823
+ * SIGINT: restore cursor, summarize partial state, exit 130.
824
+ */
825
+ export async function runJourney(ctx) {
826
+ const onSigint = () => {
827
+ ctx.stdout.write("\x1b[0m\x1b[?25h\n Interrupted. Partial state: commands may be installed; config not written.\n Finish anytime with: npx forgedock init\n");
828
+ process.exit(130);
829
+ };
830
+ process.on("SIGINT", onSigint);
831
+ try {
832
+ await preflight(ctx);
833
+ const forged = await forge(ctx);
834
+ const { draft, description } = await read(ctx);
835
+ const reviewed = await review(ctx, draft, description);
836
+ celebrate(ctx, { ...reviewed, total: forged.total, hookStatus: forged.hookStatus });
837
+ return reviewed.aborted ? 1 : 0;
838
+ } finally {
839
+ process.removeListener("SIGINT", onSigint);
840
+ }
841
+ }