forgehive 0.7.0 → 0.7.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/README.md CHANGED
@@ -14,8 +14,8 @@
14
14
  <p align="center">
15
15
  <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js ≥ 18">
16
16
  <img src="https://img.shields.io/badge/typescript-5.8-blue" alt="TypeScript">
17
- <img src="https://img.shields.io/badge/tests-217%20passing-success" alt="217 tests">
18
- <img src="https://img.shields.io/badge/bundle-205KB-lightgrey" alt="205KB bundle">
17
+ <img src="https://img.shields.io/badge/tests-267%20passing-success" alt="267 tests">
18
+ <img src="https://img.shields.io/badge/bundle-244KB-lightgrey" alt="244KB bundle">
19
19
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
20
20
  </p>
21
21
 
@@ -31,18 +31,23 @@ Claude loses all project knowledge between sessions. forgehive solves this by wr
31
31
 
32
32
  - **Stack Scanner** — detects tech stack, dependencies, and project structure automatically
33
33
  - **Persistent Memory** — context and session notes survive every restart
34
+ - **Story & Epic Tracking** — lightweight agile backlog inside `.forgehive/memory/`
35
+ - **Velocity Tracking** — sprint velocity history, rolling average, and capacity hints
34
36
  - **Party Mode** — specialized agent sets, each agent in its own isolated git worktree
35
37
  - **MCP Wiring** — preconfigured connectors for 10 services, API keys stored securely in `~/.forgehive/`
36
38
  - **Security Suite** — SAST, secret scanner, CVE check, CISO reports (GDPR/SOC2/HIPAA)
39
+ - **CI Integration** — CI health report and GitHub Actions template generator
40
+ - **Codebase Map** — file/line/import table for navigation and onboarding
41
+ - **Team Sync** — share memory across the team via a dedicated git branch
37
42
  - **AGENTS.md** — cross-tool standard (Cursor, Copilot, Gemini CLI, Codex, Windsurf)
38
43
 
39
44
  **Design principle:** writes exclusively to `.forgehive/` in the target project. No globbing outside the project root. Never overwrites files it didn't create.
40
45
 
41
46
  ---
42
47
 
43
- ## Status: Early Alpha (v0.6)
48
+ ## Status: v0.7 Stable
44
49
 
45
- This is an early release. The codebase is clean — TypeScript, 217 passing tests, single bundle but several features have not yet been tested against a real project.
50
+ 267 passing tests back the commands listed below. The v0.7 feature set has been validated end-to-end.
46
51
 
47
52
  **Stable — use with confidence:**
48
53
 
@@ -56,6 +61,16 @@ This is an early release. The codebase is clean — TypeScript, 217 passing test
56
61
  | `fh security report` — compliance reports | stable |
57
62
  | `fh memory` — show, clean, export, snapshot | stable |
58
63
  | MCP credential store (`fh mcp auth`) | stable |
64
+ | `fh ci` — CI health report + GitHub Actions template | stable |
65
+ | `fh map` — codebase structure map | stable |
66
+ | `fh onboard` — generates ONBOARDING.md | stable |
67
+ | `fh changelog` — semantic changelog from git | stable |
68
+ | `fh metrics` — developer metrics by author/type | stable |
69
+ | `fh sync` — team memory sharing via git branch | stable |
70
+ | `fh run` — background agent execution via `claude -p` | stable |
71
+ | `fh story` — story card create/list/show/done | stable |
72
+ | `fh epic` — epic create/list/show | stable |
73
+ | `fh velocity` — show history + record sprint data | stable |
59
74
 
60
75
  **Experimental — works in tests, not yet live-validated:**
61
76
 
@@ -74,7 +89,7 @@ If you run into issues, please open an issue on GitHub. The most valuable feedba
74
89
 
75
90
  - **Node.js ≥ 18** — check with `node --version`
76
91
  - **Claude Code** installed and configured (`claude` CLI available)
77
- - **git** — required for Party Mode worktrees and the guardrails hook
92
+ - **git** — required for Party Mode worktrees, `fh sync`, and the guardrails hook
78
93
 
79
94
  ---
80
95
 
@@ -94,14 +109,14 @@ This makes both `fh` and `forgehive` available globally.
94
109
  git clone https://github.com/matharnica/forgehive
95
110
  cd forgehive
96
111
  npm install
97
- npm run build # compiles to dist/cli.js (~205 KB)
112
+ npm run build # compiles to dist/cli.js (~244 KB)
98
113
  npm link # makes 'fh' available globally
99
114
  ```
100
115
 
101
116
  Verify the installation:
102
117
 
103
118
  ```bash
104
- fh --version # should print 0.6.0
119
+ fh --version # should print 0.7.1
105
120
  fh --help # lists all available commands
106
121
  ```
107
122
 
@@ -264,6 +279,91 @@ Shows `permissions.yaml` — the file access control list for each agent. Each a
264
279
 
265
280
  ---
266
281
 
282
+ ### CI
283
+
284
+ ```bash
285
+ fh ci # run CI health report (text output)
286
+ fh ci --format json # output report as JSON
287
+ fh ci --format markdown # output report as Markdown
288
+ fh ci --fail-on critical # exit 1 only on CRITICAL findings
289
+ fh ci --fail-on high # exit 1 on HIGH or CRITICAL
290
+ fh ci --fail-on any # exit 1 on any finding
291
+ fh ci --init # generate a GitHub Actions workflow template
292
+ ```
293
+
294
+ `fh ci` aggregates security scan, dependency audit, and test results into a single CI health report. Use `--init` to scaffold a `.github/workflows/forgehive.yml` file that runs the full check on every pull request. The `--fail-on` flag controls which severity level causes a non-zero exit code, giving you fine-grained control over what blocks a merge.
295
+
296
+ ---
297
+
298
+ ### Codebase Map
299
+
300
+ ```bash
301
+ fh map
302
+ ```
303
+
304
+ Prints a structured table of your codebase: file paths, line counts, and import relationships. Useful for onboarding a new engineer (or a new Claude session) to an unfamiliar codebase. Output goes to stdout; pipe it to a file if you want to save it.
305
+
306
+ ---
307
+
308
+ ### Onboarding
309
+
310
+ ```bash
311
+ fh onboard # generate ONBOARDING.md (stdout)
312
+ fh onboard --output ./ONBOARDING.md # write to file
313
+ ```
314
+
315
+ Generates a human-readable `ONBOARDING.md` from three sources: the detected stack in `capabilities.yaml`, the persistent memory in `.forgehive/memory/`, and the recent git log. The result is a self-contained document a new team member can read to understand the project architecture, conventions, and current state.
316
+
317
+ ---
318
+
319
+ ### Changelog
320
+
321
+ ```bash
322
+ fh changelog # full changelog from all commits
323
+ fh changelog --since v0.6.0 # changes since a specific tag
324
+ fh changelog --output CHANGELOG.md # write to file
325
+ ```
326
+
327
+ Parses conventional commits (`feat:`, `fix:`, `chore:`, `docs:`, etc.) and groups them into a semantic changelog. Breaking changes are surfaced separately. Requires conventional commit messages — standard for projects that use `fh git-conventions`.
328
+
329
+ ---
330
+
331
+ ### Developer Metrics
332
+
333
+ ```bash
334
+ fh metrics # all-time developer metrics
335
+ fh metrics --since 2025-01-01 # metrics since a date
336
+ ```
337
+
338
+ Shows commit activity broken down by author and commit type (feat, fix, chore, docs, test, refactor). Also shows rolling statistics: commit frequency, most active contributors, and type distribution. Useful for retrospectives and identifying contribution patterns.
339
+
340
+ ---
341
+
342
+ ### Team Memory Sync
343
+
344
+ ```bash
345
+ fh sync push # push memory to default remote/branch
346
+ fh sync push --remote origin --branch forgehive-memory # push to a specific remote branch
347
+ fh sync pull # pull memory from remote
348
+ fh sync pull --remote origin --branch forgehive-memory # pull from specific branch
349
+ ```
350
+
351
+ `fh sync` shares `.forgehive/memory/` across the team by pushing to (or pulling from) a dedicated git branch. This is an alternative to committing the memory directory into the main branch. On `pull`, existing local files are not overwritten — only new files are added, the same idempotent behavior as `fh memory snapshot import`.
352
+
353
+ ---
354
+
355
+ ### Background Agent Execution
356
+
357
+ ```bash
358
+ fh run <issue-url> # run agent on a GitHub/Linear issue URL
359
+ fh run <issue-url> --agent kai # use a specific agent
360
+ fh run <issue-url> --label ready-for-ai # filter by label
361
+ ```
362
+
363
+ `fh run` launches a background agent via `claude -p`, passes the issue content as context, and returns immediately. The agent reads the issue, applies forgehive context from `.forgehive/`, and works on the task in the background. Useful for automating repetitive issues or running agents overnight. Agent output is streamed to stdout and also written to `.forgehive/runs/`.
364
+
365
+ ---
366
+
267
367
  ### Memory
268
368
 
269
369
  Memory files live in `.forgehive/memory/` and are read by Claude at session start via the CLAUDE.md block.
@@ -297,6 +397,55 @@ Memory files live in `.forgehive/memory/` and are read by Claude at session star
297
397
 
298
398
  ---
299
399
 
400
+ ### Story Cards
401
+
402
+ Story cards are lightweight user stories stored in `.forgehive/memory/stories/`. They integrate with `/fh-sprint` for velocity-based sprint planning.
403
+
404
+ ```bash
405
+ fh story create "As a user I want to log in" # create a story (auto-numbered US-N)
406
+ fh story create "As a user I want to log in" --epic EPC-1 --points 3 # with epic + points
407
+ fh story list # list all backlog stories
408
+ fh story list --epic EPC-1 # filter by epic
409
+ fh story show US-1 # show full story card with acceptance criteria
410
+ fh story done US-1 # mark story as done
411
+ fh story done US-1 --points 5 # mark done and record actual points
412
+ ```
413
+
414
+ Each story card is a Markdown file at `.forgehive/memory/stories/US-N.md`. The card includes a title, acceptance criteria placeholder, story points, epic link, and status (`backlog` | `done`). When you run `/fh-sprint` in a Claude Code session, Claude reads the backlog and uses velocity data to suggest a realistic sprint scope.
415
+
416
+ ---
417
+
418
+ ### Epics
419
+
420
+ Epics group related stories and are stored in `.forgehive/memory/epics/`.
421
+
422
+ ```bash
423
+ fh epic create "User Authentication" # create an epic (auto-numbered EPC-N)
424
+ fh epic create "User Authentication" --goal "Users can log in securely" # with a goal statement
425
+ fh epic list # list all epics
426
+ fh epic show EPC-1 # show epic with linked stories and point total
427
+ ```
428
+
429
+ Each epic card is a Markdown file at `.forgehive/memory/epics/EPC-N.md`. `fh epic show` aggregates all stories linked to the epic and displays their status and point totals.
430
+
431
+ ---
432
+
433
+ ### Velocity
434
+
435
+ ```bash
436
+ fh velocity show # velocity history + rolling average + recommendation
437
+ fh velocity record sprint-3 --committed 21 --delivered 18 # record sprint data
438
+ ```
439
+
440
+ `fh velocity show` reads the velocity history from `.forgehive/memory/velocity.md` and prints:
441
+ - A table of past sprints (committed vs. delivered points)
442
+ - A rolling average (last 3 sprints)
443
+ - A capacity recommendation for the next sprint
444
+
445
+ `fh velocity record` appends a new entry to the velocity history. The `/fh-sprint` slash command reads this data automatically and uses it to calibrate the sprint scope it suggests.
446
+
447
+ ---
448
+
300
449
  ### Party Mode
301
450
 
302
451
  Party Mode runs specialized agent sets in parallel, each in an isolated git worktree.
@@ -320,7 +469,7 @@ fh party cleanup # remove finished worktrees
320
469
  | `security` | Vera + Sam | `/security-party` |
321
470
  | `full` | All 8 agents | `/full-party` |
322
471
 
323
- **Model mapping** — assign specific Claude models per agent:
472
+ **Multi-model routing** — `defaults.yaml` supports a `models:` key per agent set. Override via CLI:
324
473
 
325
474
  ```bash
326
475
  fh party --model-map "viktor:claude-opus-4-7,kai:claude-sonnet-4-6,sam:claude-haiku-4-5"
@@ -417,33 +566,57 @@ Clones the repo with `--depth=1`, copies all `.md` files from the `skills/expert
417
566
 
418
567
  ---
419
568
 
569
+ ### Slash Commands
570
+
571
+ The following slash commands are available inside Claude Code sessions:
572
+
573
+ | Command | Description |
574
+ |---|---|
575
+ | `/fh-sprint` | Sprint planning — reads backlog, velocity, and capacity; suggests scope using Fibonacci points (1/2/3/5/8/13); records velocity at end of sprint |
576
+ | `/fh-deploy` | Pre-deploy checklist — security scan, dependency audit, test run, final confirmation |
577
+ | `/fh-test-this` | Generate tests for the current file or selection — uses `testing-strategies` skill |
578
+ | `/fh-docs` | Generate documentation for the current file or module |
579
+ | `/party` | Start the `build` party (Viktor + Kai + Sam) |
580
+ | `/design-party` | Start the `design` party (Suki + Viktor) |
581
+ | `/review-party` | Start the `review` party (Kai + Sam + Eli) |
582
+ | `/security-party` | Start the `security` party (Vera + Sam) |
583
+ | `/full-party` | Start all 8 agents |
584
+
585
+ **`/fh-sprint` in detail:** Reads `.forgehive/memory/stories/` for backlog items, `.forgehive/memory/velocity.md` for historical capacity, and `.forgehive/memory/epics/` for epic context. Suggests a sprint scope as a list of story cards with Fibonacci point estimates. When you close the sprint, it prompts you to record delivered points so velocity history stays up to date. If Linear or GitHub MCP servers are connected, it can pull issues directly from those services.
586
+
587
+ ---
588
+
420
589
  ## What `fh init` Creates
421
590
 
422
591
  ```
423
592
  my-project/
424
593
  .forgehive/
425
- capabilities.yaml detected stack (status: draft confirmed)
426
- scan-result.yaml raw scanner output
427
- .scan-hash checksum for fh scan --check
594
+ capabilities.yaml <- detected stack (status: draft -> confirmed)
595
+ scan-result.yaml <- raw scanner output
596
+ .scan-hash <- checksum for fh scan --check
428
597
  harness/
429
- architecture.md codebase overview for Claude
430
- constraints.yaml max_lines, require_tests, style rules
431
- permissions.yaml per-agent file access control (read/write/deny)
598
+ architecture.md <- codebase overview for Claude
599
+ constraints.yaml <- max_lines, require_tests, style rules
600
+ permissions.yaml <- per-agent file access control (read/write/deny)
432
601
  memory/
433
- MEMORY.md index of all memory files
434
- project.md project context and open decisions
435
- feedback.md corrections + confirmed approaches
436
- stack.md stack details the scanner can't detect
437
- adrs/ architecture decision records
602
+ MEMORY.md <- index of all memory files
603
+ project.md <- project context and open decisions
604
+ feedback.md <- corrections + confirmed approaches
605
+ stack.md <- stack details the scanner can't detect
606
+ adrs/ <- architecture decision records
607
+ stories/ <- story cards (US-N.md)
608
+ epics/ <- epic cards (EPC-N.md)
609
+ velocity.md <- sprint velocity history
438
610
  skills/
439
- generated/ AI-generated skills (fh skills regen)
440
- expert/ 16 preinstalled expert skills
441
- workflows/ MCP workflow skills
442
- worktrees/ isolated git worktrees (fh party run)
443
- audit.log ← JSONL audit trail (fh security commands)
444
- cost-config.yaml ← spend limits (fh cost --limit/--alert)
445
- AGENTS.md ← cross-tool agent standard
446
- CLAUDE.md forgehive block inserted, rest preserved
611
+ generated/ <- AI-generated skills (fh skills regen)
612
+ expert/ <- 16 preinstalled expert skills
613
+ workflows/ <- MCP workflow skills
614
+ worktrees/ <- isolated git worktrees (fh party run)
615
+ runs/ <- background agent output (fh run)
616
+ audit.log <- JSONL audit trail (fh security commands)
617
+ cost-config.yaml <- spend limits (fh cost --limit/--alert)
618
+ AGENTS.md <- cross-tool agent standard
619
+ CLAUDE.md <- forgehive block inserted, rest preserved
447
620
  ```
448
621
 
449
622
  ---
@@ -520,7 +693,7 @@ The hook fires on every bash command execution in Claude Code and exits 1 if a m
520
693
  cd my-project
521
694
  fh init
522
695
  fh confirm
523
- # Open Claude Code Claude now has full context
696
+ # Open Claude Code -> Claude now has full context
524
697
  ```
525
698
 
526
699
  ### After adding new dependencies
@@ -539,6 +712,49 @@ fh security deps # check for new CVEs in dependencies
539
712
  fh security report gdpr # update compliance report
540
713
  ```
541
714
 
715
+ ### CI integration
716
+
717
+ ```bash
718
+ fh ci --init # scaffold .github/workflows/forgehive.yml
719
+ fh ci --format markdown --fail-on high # use in existing CI scripts
720
+ ```
721
+
722
+ ### Generating onboarding documentation
723
+
724
+ ```bash
725
+ fh map # review codebase structure
726
+ fh onboard --output ONBOARDING.md # generate onboarding document
727
+ git add ONBOARDING.md
728
+ git commit -m "docs: add forgehive-generated onboarding"
729
+ ```
730
+
731
+ ### Generating a changelog
732
+
733
+ ```bash
734
+ fh changelog --since v0.6.0 --output CHANGELOG.md
735
+ ```
736
+
737
+ ### Sprint planning
738
+
739
+ ```bash
740
+ # Set up your backlog
741
+ fh epic create "User Authentication" --goal "Users can log in securely"
742
+ fh story create "As a user I want to log in" --epic EPC-1 --points 3
743
+ fh story create "As a user I want to reset my password" --epic EPC-1 --points 2
744
+
745
+ # Check current velocity
746
+ fh velocity show
747
+
748
+ # Open Claude Code and run:
749
+ # /fh-sprint
750
+ # Claude reads velocity + backlog and suggests scope
751
+
752
+ # At end of sprint
753
+ fh velocity record sprint-1 --committed 13 --delivered 11
754
+ fh story done US-1
755
+ fh story done US-2
756
+ ```
757
+
542
758
  ### Sharing context with the team
543
759
 
544
760
  ```bash
@@ -548,6 +764,11 @@ git commit -m "chore: update shared forgehive context"
548
764
 
549
765
  # On another machine:
550
766
  fh memory snapshot import ./team-context.json
767
+
768
+ # Or use the sync approach (no committed file):
769
+ fh sync push --remote origin --branch forgehive-memory
770
+ # teammate runs:
771
+ fh sync pull --remote origin --branch forgehive-memory
551
772
  ```
552
773
 
553
774
  ### Starting a multi-agent build session
@@ -555,8 +776,8 @@ fh memory snapshot import ./team-context.json
555
776
  ```bash
556
777
  fh party --set build
557
778
  fh party run
558
- # Creates worktrees for Viktor (architect), Kai (engineer), Sam (QA)
559
- # Each Claude Code session opens in its own isolated worktree
779
+ # -> Creates worktrees for Viktor (architect), Kai (engineer), Sam (QA)
780
+ # -> Each Claude Code session opens in its own isolated worktree
560
781
 
561
782
  fh party status # check what's running
562
783
  fh party cleanup # clean up after the session
@@ -575,6 +796,14 @@ fh mcp auth add github GITHUB_TOKEN=ghp_...
575
796
  fh mcp auth list # verify both services are stored
576
797
  ```
577
798
 
799
+ ### Running a background agent on an issue
800
+
801
+ ```bash
802
+ fh run https://github.com/my-org/my-repo/issues/42 --agent kai
803
+ # Kai reads the issue, applies forgehive context, works in background
804
+ # Output streamed to stdout and saved to .forgehive/runs/
805
+ ```
806
+
578
807
  ---
579
808
 
580
809
  ## Configuration Files
@@ -613,13 +842,13 @@ Global credential store (chmod 600). Managed exclusively via `fh mcp auth` comma
613
842
  |---|---|
614
843
  | Runtime | Node.js ≥ 18, ESM |
615
844
  | Language | TypeScript |
616
- | Build | esbuild `dist/cli.js` (~205 KB, single bundle) |
845
+ | Build | esbuild -> `dist/cli.js` (~244 KB, single bundle) |
617
846
  | Type check | `tsc --noEmit` |
618
- | Tests | `node:test` (native) + tsx ESM loader, 217 tests |
847
+ | Tests | `node:test` (native) + tsx ESM loader, 267 tests |
619
848
  | Dependencies | `js-yaml` (sole runtime dependency) |
620
849
 
621
850
  ```bash
622
- npm run build # esbuild dist/cli.js
851
+ npm run build # esbuild -> dist/cli.js
623
852
  npm run typecheck # tsc --noEmit
624
853
  npm test # node --import tsx/esm --test test/*.test.ts
625
854
  ```
package/dist/cli.js CHANGED
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
2751
2751
 
2752
2752
  // src/cli.ts
2753
2753
  init_js_yaml();
2754
- import fs27 from "node:fs";
2755
- import path28 from "node:path";
2754
+ import fs30 from "node:fs";
2755
+ import path31 from "node:path";
2756
2756
 
2757
2757
  // src/scanner.ts
2758
2758
  import fs from "node:fs";
@@ -6196,26 +6196,359 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
6196
6196
  };
6197
6197
  }
6198
6198
 
6199
+ // src/stories.ts
6200
+ init_js_yaml();
6201
+ import fs27 from "node:fs";
6202
+ import path28 from "node:path";
6203
+ function nextStoryId(storiesDir) {
6204
+ if (!fs27.existsSync(storiesDir)) return "US-1";
6205
+ const existing = fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6206
+ const max = existing.length > 0 ? Math.max(...existing) : 0;
6207
+ return `US-${max + 1}`;
6208
+ }
6209
+ function storyToMarkdown(story) {
6210
+ const frontmatter = jsYaml.dump({
6211
+ id: story.id,
6212
+ title: story.title,
6213
+ asA: story.asA,
6214
+ iWant: story.iWant,
6215
+ soThat: story.soThat,
6216
+ points: story.points,
6217
+ epicId: story.epicId,
6218
+ status: story.status
6219
+ });
6220
+ const acLines = story.acceptanceCriteria.map((c) => `- [ ] ${c}`).join("\n");
6221
+ return `---
6222
+ ${frontmatter}---
6223
+
6224
+ # ${story.id}: ${story.title}
6225
+
6226
+ ## User Story
6227
+
6228
+ Als **${story.asA}** m\xF6chte ich **${story.iWant}**, damit **${story.soThat}**.
6229
+
6230
+ ## Acceptance Criteria
6231
+
6232
+ ${acLines || "- [ ] (noch nicht definiert)"}
6233
+ `;
6234
+ }
6235
+ function parseStoryFile(filePath) {
6236
+ try {
6237
+ const content = fs27.readFileSync(filePath, "utf8");
6238
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6239
+ if (!match) return null;
6240
+ const data = jsYaml.load(match[1]);
6241
+ return {
6242
+ id: data.id ?? "",
6243
+ title: data.title ?? "",
6244
+ asA: data.asA ?? "",
6245
+ iWant: data.iWant ?? "",
6246
+ soThat: data.soThat ?? "",
6247
+ acceptanceCriteria: data.acceptanceCriteria ?? [],
6248
+ points: data.points ?? null,
6249
+ epicId: data.epicId ?? null,
6250
+ status: data.status ?? "backlog"
6251
+ };
6252
+ } catch {
6253
+ return null;
6254
+ }
6255
+ }
6256
+ function createStory(storiesDir, title, epicId) {
6257
+ fs27.mkdirSync(storiesDir, { recursive: true });
6258
+ const id = nextStoryId(storiesDir);
6259
+ const story = {
6260
+ id,
6261
+ title,
6262
+ asA: "",
6263
+ iWant: title,
6264
+ soThat: "",
6265
+ acceptanceCriteria: [],
6266
+ points: null,
6267
+ epicId: epicId ?? null,
6268
+ status: "backlog"
6269
+ };
6270
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6271
+ return story;
6272
+ }
6273
+ function listStories(storiesDir) {
6274
+ if (!fs27.existsSync(storiesDir)) return [];
6275
+ return fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path28.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
6276
+ const na = parseInt(a.id.replace("US-", ""), 10);
6277
+ const nb = parseInt(b.id.replace("US-", ""), 10);
6278
+ return na - nb;
6279
+ });
6280
+ }
6281
+ function getStory(storiesDir, id) {
6282
+ const filePath = path28.join(storiesDir, `${id}.md`);
6283
+ if (!fs27.existsSync(filePath)) return null;
6284
+ return parseStoryFile(filePath);
6285
+ }
6286
+ function updateStoryPoints(storiesDir, id, points) {
6287
+ const story = getStory(storiesDir, id);
6288
+ if (!story) throw new Error(`Story ${id} nicht gefunden`);
6289
+ story.points = points;
6290
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6291
+ }
6292
+ function updateStoryStatus(storiesDir, id, status) {
6293
+ const story = getStory(storiesDir, id);
6294
+ if (!story) throw new Error(`Story ${id} nicht gefunden`);
6295
+ story.status = status;
6296
+ fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
6297
+ }
6298
+ function formatStoryCard(story) {
6299
+ const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
6300
+ const epic = story.epicId ? ` \xB7 ${story.epicId}` : "";
6301
+ const lines = [];
6302
+ lines.push(`## ${story.id}: ${story.title}${points}${epic}`);
6303
+ lines.push(`**Status:** ${story.status}`);
6304
+ if (story.asA || story.iWant)
6305
+ lines.push(`**Story:** Als ${story.asA || "Nutzer"} m\xF6chte ich ${story.iWant}${story.soThat ? `, damit ${story.soThat}` : ""}.`);
6306
+ if (story.acceptanceCriteria.length > 0) {
6307
+ lines.push("**Acceptance Criteria:**");
6308
+ for (const ac of story.acceptanceCriteria) lines.push(`- ${ac}`);
6309
+ }
6310
+ return lines.join("\n");
6311
+ }
6312
+
6313
+ // src/epics.ts
6314
+ init_js_yaml();
6315
+ import fs28 from "node:fs";
6316
+ import path29 from "node:path";
6317
+ function nextEpicId(epicsDir) {
6318
+ if (!fs28.existsSync(epicsDir)) return "EPC-1";
6319
+ const existing = fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
6320
+ const max = existing.length > 0 ? Math.max(...existing) : 0;
6321
+ return `EPC-${max + 1}`;
6322
+ }
6323
+ function epicToMarkdown(epic) {
6324
+ const frontmatter = jsYaml.dump({
6325
+ id: epic.id,
6326
+ title: epic.title,
6327
+ goal: epic.goal,
6328
+ stories: epic.stories,
6329
+ status: epic.status
6330
+ });
6331
+ const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
6332
+ return `---
6333
+ ${frontmatter}---
6334
+
6335
+ # ${epic.id}: ${epic.title}
6336
+
6337
+ **Ziel:** ${epic.goal || "(noch nicht definiert)"}
6338
+
6339
+ ## Stories
6340
+
6341
+ ${storyLines || "(noch keine Stories)"}
6342
+ `;
6343
+ }
6344
+ function parseEpicFile(filePath) {
6345
+ try {
6346
+ const content = fs28.readFileSync(filePath, "utf8");
6347
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
6348
+ if (!match) return null;
6349
+ const data = jsYaml.load(match[1]);
6350
+ return {
6351
+ id: data.id ?? "",
6352
+ title: data.title ?? "",
6353
+ goal: data.goal ?? "",
6354
+ stories: data.stories ?? [],
6355
+ status: data.status ?? "active"
6356
+ };
6357
+ } catch {
6358
+ return null;
6359
+ }
6360
+ }
6361
+ function createEpic(epicsDir, title, goal) {
6362
+ fs28.mkdirSync(epicsDir, { recursive: true });
6363
+ const id = nextEpicId(epicsDir);
6364
+ const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
6365
+ fs28.writeFileSync(path29.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
6366
+ return epic;
6367
+ }
6368
+ function listEpics(epicsDir) {
6369
+ if (!fs28.existsSync(epicsDir)) return [];
6370
+ return fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path29.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
6371
+ const na = parseInt(a.id.replace("EPC-", ""), 10);
6372
+ const nb = parseInt(b.id.replace("EPC-", ""), 10);
6373
+ return na - nb;
6374
+ });
6375
+ }
6376
+ function getEpic(epicsDir, id) {
6377
+ const filePath = path29.join(epicsDir, `${id}.md`);
6378
+ if (!fs28.existsSync(filePath)) return null;
6379
+ return parseEpicFile(filePath);
6380
+ }
6381
+ function formatEpicCard(epic, stories) {
6382
+ const lines = [];
6383
+ lines.push(`## ${epic.id}: ${epic.title} [${epic.status}]`);
6384
+ if (epic.goal) lines.push(`**Ziel:** ${epic.goal}`);
6385
+ lines.push(`**Stories:** ${epic.stories.length}`);
6386
+ if (stories && stories.length > 0) {
6387
+ const total = stories.reduce((sum, s) => sum + (s.points ?? 0), 0);
6388
+ lines.push(`**Punkte:** ${total}`);
6389
+ lines.push("");
6390
+ for (const s of stories) {
6391
+ const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
6392
+ lines.push(` - ${s.id}${pts} \u2014 ${s.title} [${s.status}]`);
6393
+ }
6394
+ } else if (epic.stories.length > 0) {
6395
+ for (const id of epic.stories) lines.push(` - ${id}`);
6396
+ }
6397
+ return lines.join("\n");
6398
+ }
6399
+
6400
+ // src/velocity.ts
6401
+ import fs29 from "node:fs";
6402
+ import path30 from "node:path";
6403
+ var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
6404
+ function recordVelocity(velocityFile, sprint, committed, delivered) {
6405
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6406
+ const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
6407
+ const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
6408
+ `;
6409
+ if (!fs29.existsSync(velocityFile)) {
6410
+ fs29.mkdirSync(path30.dirname(velocityFile), { recursive: true });
6411
+ fs29.writeFileSync(velocityFile, HEADER + row, "utf8");
6412
+ } else {
6413
+ fs29.appendFileSync(velocityFile, row, "utf8");
6414
+ }
6415
+ }
6416
+ function getVelocityHistory(velocityFile) {
6417
+ if (!fs29.existsSync(velocityFile)) return [];
6418
+ const content = fs29.readFileSync(velocityFile, "utf8");
6419
+ const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
6420
+ return rows.map((row) => {
6421
+ const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
6422
+ const sprintMatch = cells[0]?.match(/Sprint (\d+)/);
6423
+ return {
6424
+ sprint: sprintMatch ? parseInt(sprintMatch[1], 10) : 0,
6425
+ date: cells[1] ?? "",
6426
+ committed: parseInt(cells[2] ?? "0", 10),
6427
+ delivered: parseInt(cells[3] ?? "0", 10)
6428
+ };
6429
+ }).filter((r) => r.sprint > 0);
6430
+ }
6431
+ function getRollingAverage(history, window = 3) {
6432
+ if (history.length === 0) return 0;
6433
+ const recent = history.slice(-window);
6434
+ const sum = recent.reduce((acc, s) => acc + s.delivered, 0);
6435
+ return Math.round(sum / recent.length);
6436
+ }
6437
+ function formatVelocityReport(history) {
6438
+ if (history.length === 0)
6439
+ return "Noch keine Velocity-Daten vorhanden.\n\nStarte mit: `fh velocity record <sprint> --committed N --delivered N`";
6440
+ const avg3 = getRollingAverage(history, 3);
6441
+ const lines = [];
6442
+ lines.push("# Sprint Velocity");
6443
+ lines.push("");
6444
+ lines.push(`**Rolling Average (letzte 3 Sprints):** ${avg3} Punkte`);
6445
+ lines.push("");
6446
+ lines.push("| Sprint | Datum | Committed | Delivered | Rate |");
6447
+ lines.push("|---|---|---|---|---|");
6448
+ for (const s of history) {
6449
+ const rate = s.committed > 0 ? Math.round(s.delivered / s.committed * 100) : 0;
6450
+ lines.push(`| Sprint ${s.sprint} | ${s.date} | ${s.committed} | ${s.delivered} | ${rate}% |`);
6451
+ }
6452
+ lines.push("");
6453
+ lines.push(`**Empfehlung f\xFCr n\xE4chsten Sprint:** ~${avg3} Punkte einplanen`);
6454
+ return lines.join("\n");
6455
+ }
6456
+
6199
6457
  // src/cli.ts
6200
6458
  var [, , command, subcommand, ...rest] = process.argv;
6201
6459
  var projectRoot = process.cwd();
6202
- var forgehiveDir = path28.join(projectRoot, ".forgehive");
6460
+ var forgehiveDir = path31.join(projectRoot, ".forgehive");
6203
6461
  if (command === "--version" || command === "-v") {
6204
- console.log("0.7.0");
6462
+ console.log("0.7.2");
6463
+ process.exit(0);
6464
+ }
6465
+ if (command === "--help" || command === "-h" || command === "help") {
6466
+ console.log(`
6467
+ forgehive v0.7.2 \u2014 Context-aware AI development environment
6468
+
6469
+ USAGE
6470
+ fh <command> [subcommand] [options]
6471
+
6472
+ SETUP
6473
+ fh init Set up forgehive in the current project
6474
+ fh confirm Activate capabilities (draft \u2192 confirmed)
6475
+ fh rollback Remove forgehive from the project
6476
+ fh status Show current project state
6477
+ fh scan --update Re-scan project after changes
6478
+ fh scan --check Check if scan is still current
6479
+
6480
+ SECURITY
6481
+ fh security scan Secrets + SAST scan
6482
+ fh security deps CVE check (npm audit)
6483
+ fh security report [gdpr|soc2|hipaa] Compliance report
6484
+ fh security audit Show audit trail
6485
+ fh security permissions Show agent file permissions
6486
+
6487
+ CI
6488
+ fh ci Run CI health report
6489
+ fh ci --format json|markdown Output format
6490
+ fh ci --fail-on critical|high|any Failure threshold
6491
+ fh ci --init Generate GitHub Actions workflow
6492
+
6493
+ CODEBASE
6494
+ fh map Codebase structure map
6495
+ fh onboard [--output path] Generate ONBOARDING.md
6496
+ fh changelog [--since tag] Semantic changelog from git
6497
+ fh metrics [--since date] Developer productivity metrics
6498
+
6499
+ SPRINT PLANNING
6500
+ fh story create <title> [--epic EPC-N] [--points N]
6501
+ fh story list [--epic EPC-N]
6502
+ fh story show <US-N>
6503
+ fh story sprint <US-N> Mark story as in-sprint
6504
+ fh story done <US-N> [--points N]
6505
+ fh epic create <title> [--goal <text>]
6506
+ fh epic list
6507
+ fh epic show <EPC-N>
6508
+ fh velocity show Velocity history + rolling average
6509
+ fh velocity record <N> --committed N --delivered N
6510
+
6511
+ TEAM
6512
+ fh sync push|pull [--remote origin --branch forgehive-memory]
6513
+ fh run <issue-url> [--agent name] [--label label]
6514
+ fh memory show|clean|export|prune|snapshot
6515
+ fh memory adr list|"<title>"
6516
+
6517
+ AGENTS & MCP
6518
+ fh party [--set name|run|status|cleanup]
6519
+ fh wire <service> Configure MCP server
6520
+ fh mcp auth add|list|remove Manage credentials
6521
+ fh mcp search <query> Search MCP registry
6522
+ fh skills list|regen|pull <url>
6523
+
6524
+ COST
6525
+ fh cost [today|week|all]
6526
+ fh cost --limit N --alert N
6527
+
6528
+ fh --version Show version
6529
+ fh --help Show this help
6530
+ `);
6205
6531
  process.exit(0);
6206
6532
  }
6207
6533
  function loadClaudeMdBlock() {
6208
- const templatePath = path28.join(
6209
- path28.dirname(new URL(import.meta.url).pathname),
6534
+ const templatePath = path31.join(
6535
+ path31.dirname(new URL(import.meta.url).pathname),
6210
6536
  "..",
6211
6537
  "forgehive",
6212
6538
  "templates",
6213
6539
  "claude-md.block.md"
6214
6540
  );
6215
- if (!fs27.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6216
- return fs27.readFileSync(templatePath, "utf8");
6541
+ if (!fs30.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6542
+ return fs30.readFileSync(templatePath, "utf8");
6217
6543
  }
6218
6544
  if (command === "init") {
6545
+ const forgehiveDirExists = fs30.existsSync(forgehiveDir);
6546
+ if (forgehiveDirExists && !rest.includes("--force")) {
6547
+ console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
6548
+ console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
6549
+ console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
6550
+ process.exit(0);
6551
+ }
6219
6552
  console.log("\u{1F50D} Analysiere Projekt...\n");
6220
6553
  const scanResult = scan(projectRoot);
6221
6554
  const tierCount = [1, 2, 3].map((t) => scanResult.signals.filter((s) => s.tier === t).length);
@@ -6227,9 +6560,9 @@ if (command === "init") {
6227
6560
  const block = loadClaudeMdBlock();
6228
6561
  writeForgehiveDir(projectRoot, scanResult, capMap, block);
6229
6562
  const hash = computeHash(projectRoot);
6230
- fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6231
- const runtimeDir = path28.join(
6232
- path28.dirname(new URL(import.meta.url).pathname),
6563
+ fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6564
+ const runtimeDir = path31.join(
6565
+ path31.dirname(new URL(import.meta.url).pathname),
6233
6566
  "..",
6234
6567
  "forgehive"
6235
6568
  );
@@ -6261,7 +6594,7 @@ if (command === "init") {
6261
6594
  process.exit(1);
6262
6595
  }
6263
6596
  } else if (command === "memory") {
6264
- if (!fs27.existsSync(forgehiveDir)) {
6597
+ if (!fs30.existsSync(forgehiveDir)) {
6265
6598
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6266
6599
  process.exit(1);
6267
6600
  }
@@ -6270,7 +6603,7 @@ if (command === "init") {
6270
6603
  } else if (subcommand === "clean") {
6271
6604
  cleanMemory(forgehiveDir);
6272
6605
  } else if (subcommand === "export") {
6273
- const outputPath = rest[0] ?? path28.join(projectRoot, "forgehive-memory-export.md");
6606
+ const outputPath = rest[0] ?? path31.join(projectRoot, "forgehive-memory-export.md");
6274
6607
  try {
6275
6608
  exportMemory(forgehiveDir, outputPath);
6276
6609
  } catch (err) {
@@ -6318,7 +6651,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6318
6651
  } else if (subcommand === "snapshot") {
6319
6652
  const snapAction = rest[0];
6320
6653
  if (snapAction === "export") {
6321
- const outPath = rest[1] ?? path28.join(
6654
+ const outPath = rest[1] ?? path31.join(
6322
6655
  projectRoot,
6323
6656
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
6324
6657
  );
@@ -6358,11 +6691,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6358
6691
  process.exit(1);
6359
6692
  }
6360
6693
  } else if (command === "scan" && subcommand === "--update") {
6361
- if (!fs27.existsSync(forgehiveDir)) {
6694
+ if (!fs30.existsSync(forgehiveDir)) {
6362
6695
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6363
6696
  process.exit(1);
6364
6697
  }
6365
- const savedHash = fs27.existsSync(path28.join(forgehiveDir, ".scan-hash")) ? fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6698
+ const savedHash = fs30.existsSync(path31.join(forgehiveDir, ".scan-hash")) ? fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6366
6699
  const currentHash = computeHash(projectRoot);
6367
6700
  if (savedHash === currentHash) {
6368
6701
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -6370,7 +6703,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6370
6703
  }
6371
6704
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
6372
6705
  const oldDoc = jsYaml.load(
6373
- fs27.readFileSync(path28.join(forgehiveDir, "capabilities.yaml"), "utf8")
6706
+ fs30.readFileSync(path31.join(forgehiveDir, "capabilities.yaml"), "utf8")
6374
6707
  );
6375
6708
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
6376
6709
  const scanResult = scan(projectRoot);
@@ -6390,16 +6723,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6390
6723
  console.log();
6391
6724
  const block = loadClaudeMdBlock();
6392
6725
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
6393
- fs27.writeFileSync(path28.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6726
+ fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6394
6727
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
6395
6728
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
6396
6729
  }
6397
6730
  } else if (command === "scan" && subcommand === "--check") {
6398
- if (!fs27.existsSync(path28.join(forgehiveDir, ".scan-hash"))) {
6731
+ if (!fs30.existsSync(path31.join(forgehiveDir, ".scan-hash"))) {
6399
6732
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
6400
6733
  process.exit(1);
6401
6734
  }
6402
- const saved = fs27.readFileSync(path28.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6735
+ const saved = fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6403
6736
  const current = computeHash(projectRoot);
6404
6737
  if (saved !== current) {
6405
6738
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -6408,7 +6741,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6408
6741
  }
6409
6742
  console.log("\u2713 capabilities.yaml ist aktuell");
6410
6743
  } else if (command === "skills") {
6411
- if (!fs27.existsSync(forgehiveDir)) {
6744
+ if (!fs30.existsSync(forgehiveDir)) {
6412
6745
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6413
6746
  process.exit(1);
6414
6747
  }
@@ -6444,7 +6777,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6444
6777
  process.exit(1);
6445
6778
  }
6446
6779
  } else if (command === "party") {
6447
- if (!fs27.existsSync(forgehiveDir)) {
6780
+ if (!fs30.existsSync(forgehiveDir)) {
6448
6781
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6449
6782
  process.exit(1);
6450
6783
  }
@@ -6565,7 +6898,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6565
6898
  }
6566
6899
  }
6567
6900
  } else if (command === "wire") {
6568
- if (!fs27.existsSync(forgehiveDir)) {
6901
+ if (!fs30.existsSync(forgehiveDir)) {
6569
6902
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6570
6903
  process.exit(1);
6571
6904
  }
@@ -6604,7 +6937,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6604
6937
  const limitIdx = allArgs.indexOf("--limit");
6605
6938
  const alertIdx = allArgs.indexOf("--alert");
6606
6939
  if (limitIdx !== -1) {
6607
- if (!fs27.existsSync(forgehiveDir)) {
6940
+ if (!fs30.existsSync(forgehiveDir)) {
6608
6941
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6609
6942
  process.exit(1);
6610
6943
  }
@@ -6630,14 +6963,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6630
6963
  }
6631
6964
  const sessions = parseCostSessions(projectRoot);
6632
6965
  console.log(formatCostReport(sessions, range));
6633
- if (fs27.existsSync(forgehiveDir)) {
6966
+ if (fs30.existsSync(forgehiveDir)) {
6634
6967
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
6635
6968
  const status = checkSpendStatus(forgehiveDir, total);
6636
6969
  if (status.message) console.log("\n" + status.message);
6637
6970
  }
6638
6971
  }
6639
6972
  } else if (command === "watch") {
6640
- if (!fs27.existsSync(forgehiveDir)) {
6973
+ if (!fs30.existsSync(forgehiveDir)) {
6641
6974
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6642
6975
  process.exit(1);
6643
6976
  }
@@ -6653,7 +6986,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6653
6986
  process.exit(0);
6654
6987
  });
6655
6988
  } else if (command === "mcp") {
6656
- if (!fs27.existsSync(forgehiveDir)) {
6989
+ if (!fs30.existsSync(forgehiveDir)) {
6657
6990
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6658
6991
  process.exit(1);
6659
6992
  }
@@ -6764,7 +7097,7 @@ Setze diese Umgebungsvariablen:`);
6764
7097
  process.exit(1);
6765
7098
  }
6766
7099
  } else if (command === "security") {
6767
- if (!fs27.existsSync(forgehiveDir)) {
7100
+ if (!fs30.existsSync(forgehiveDir)) {
6768
7101
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6769
7102
  process.exit(1);
6770
7103
  }
@@ -6850,8 +7183,8 @@ Setze diese Umgebungsvariablen:`);
6850
7183
  `);
6851
7184
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
6852
7185
  const text = formatSecurityReport(report);
6853
- const reportPath = path28.join(forgehiveDir, "security-report.md");
6854
- fs27.writeFileSync(reportPath, text, "utf8");
7186
+ const reportPath = path31.join(forgehiveDir, "security-report.md");
7187
+ fs30.writeFileSync(reportPath, text, "utf8");
6855
7188
  console.log(text);
6856
7189
  console.log(`
6857
7190
  \u2713 Report gespeichert: ${reportPath}`);
@@ -6863,8 +7196,8 @@ Setze diese Umgebungsvariablen:`);
6863
7196
  } else if (subcommand === "permissions") {
6864
7197
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
6865
7198
  writePermissions2(forgehiveDir);
6866
- const permPath = path28.join(forgehiveDir, "harness", "permissions.yaml");
6867
- console.log(fs27.readFileSync(permPath, "utf8"));
7199
+ const permPath = path31.join(forgehiveDir, "harness", "permissions.yaml");
7200
+ console.log(fs30.readFileSync(permPath, "utf8"));
6868
7201
  } else {
6869
7202
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
6870
7203
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
@@ -6876,35 +7209,35 @@ Setze diese Umgebungsvariablen:`);
6876
7209
  const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
6877
7210
  const initFlag = allCiArgs.includes("--init");
6878
7211
  if (initFlag) {
6879
- const ghDir = path28.join(projectRoot, ".github", "workflows");
6880
- fs27.mkdirSync(ghDir, { recursive: true });
6881
- const outPath = path28.join(ghDir, "forgehive.yml");
6882
- fs27.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
7212
+ const ghDir = path31.join(projectRoot, ".github", "workflows");
7213
+ fs30.mkdirSync(ghDir, { recursive: true });
7214
+ const outPath = path31.join(ghDir, "forgehive.yml");
7215
+ fs30.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
6883
7216
  console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
6884
7217
  } else {
6885
7218
  const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
6886
7219
  const output = formatCiReport(report, format);
6887
7220
  console.log(output);
6888
7221
  if (format === "json") {
6889
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6890
- fs27.writeFileSync(path28.join(forgehiveDir, "ci-report.json"), output, "utf8");
7222
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7223
+ fs30.writeFileSync(path31.join(forgehiveDir, "ci-report.json"), output, "utf8");
6891
7224
  }
6892
7225
  if (report.status === "fail") process.exit(1);
6893
7226
  }
6894
7227
  } else if (command === "map") {
6895
7228
  const map2 = generateMap(projectRoot);
6896
7229
  const md = formatMap(map2);
6897
- const mapPath = path28.join(forgehiveDir, "map.md");
6898
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6899
- fs27.writeFileSync(mapPath, md, "utf8");
7230
+ const mapPath = path31.join(forgehiveDir, "map.md");
7231
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7232
+ fs30.writeFileSync(mapPath, md, "utf8");
6900
7233
  console.log(md);
6901
7234
  console.log(`
6902
7235
  \u2714 Codebase-Map gespeichert: ${mapPath}`);
6903
7236
  } else if (command === "onboard") {
6904
7237
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
6905
- const outputPath = outputArg ?? path28.join(projectRoot, "ONBOARDING.md");
7238
+ const outputPath = outputArg ?? path31.join(projectRoot, "ONBOARDING.md");
6906
7239
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
6907
- fs27.writeFileSync(outputPath, doc, "utf8");
7240
+ fs30.writeFileSync(outputPath, doc, "utf8");
6908
7241
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
6909
7242
  } else if (command === "changelog") {
6910
7243
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
@@ -6914,28 +7247,28 @@ Setze diese Umgebungsvariablen:`);
6914
7247
  const commits = parseGitLog(rawLog);
6915
7248
  let version = "unreleased";
6916
7249
  try {
6917
- const pkgPath = path28.join(projectRoot, "package.json");
6918
- if (fs27.existsSync(pkgPath)) {
6919
- const pkg = JSON.parse(fs27.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
7250
+ const pkgPath = path31.join(projectRoot, "package.json");
7251
+ if (fs30.existsSync(pkgPath)) {
7252
+ const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
6920
7253
  version = pkg.version ?? "unreleased";
6921
7254
  }
6922
7255
  } catch {
6923
7256
  }
6924
7257
  const md = formatChangelog(commits, version);
6925
- const outputPath = outputArg ?? path28.join(projectRoot, "CHANGELOG.md");
7258
+ const outputPath = outputArg ?? path31.join(projectRoot, "CHANGELOG.md");
6926
7259
  let existing = "";
6927
- if (fs27.existsSync(outputPath)) existing = fs27.readFileSync(outputPath, "utf8");
6928
- fs27.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7260
+ if (fs30.existsSync(outputPath)) existing = fs30.readFileSync(outputPath, "utf8");
7261
+ fs30.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
6929
7262
  console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
6930
7263
  console.log(` ${commits.length} Commits verarbeitet`);
6931
7264
  } else if (command === "metrics") {
6932
7265
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
6933
7266
  const rawLog = getMetricsGitLog(projectRoot, sinceArg);
6934
7267
  const stats = parseCommitStats(rawLog);
6935
- const md = formatMetrics(stats, path28.basename(projectRoot));
6936
- const metricsPath = path28.join(forgehiveDir, "metrics.md");
6937
- fs27.mkdirSync(forgehiveDir, { recursive: true });
6938
- fs27.writeFileSync(metricsPath, md, "utf8");
7268
+ const md = formatMetrics(stats, path31.basename(projectRoot));
7269
+ const metricsPath = path31.join(forgehiveDir, "metrics.md");
7270
+ fs30.mkdirSync(forgehiveDir, { recursive: true });
7271
+ fs30.writeFileSync(metricsPath, md, "utf8");
6939
7272
  console.log(md);
6940
7273
  console.log(`
6941
7274
  \u2714 Metrics gespeichert: ${metricsPath}`);
@@ -6969,10 +7302,125 @@ Setze diese Umgebungsvariablen:`);
6969
7302
  const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
6970
7303
  console.log(`\u2714 ${result.message}`);
6971
7304
  console.log(` fh run status \u2014 aktive Sessions anzeigen`);
7305
+ } else if (command === "story") {
7306
+ const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7307
+ if (subcommand === "create") {
7308
+ const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7309
+ const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
7310
+ const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
7311
+ if (!title) {
7312
+ console.error("Usage: fh story create <titel> [--epic EPC-N] [--points N]");
7313
+ process.exit(1);
7314
+ }
7315
+ const story = createStory(storiesDir, title, epicArg);
7316
+ if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
7317
+ console.log(`\u2714 ${story.id} erstellt: ${path31.join(storiesDir, story.id + ".md")}`);
7318
+ console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
7319
+ } else if (subcommand === "list") {
7320
+ const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
7321
+ let stories = listStories(storiesDir);
7322
+ if (epicFilter) stories = stories.filter((s) => s.epicId === epicFilter);
7323
+ if (stories.length === 0) {
7324
+ console.log("Keine Stories gefunden.");
7325
+ } else {
7326
+ for (const s of stories) {
7327
+ const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
7328
+ const epic = s.epicId ? ` (${s.epicId})` : "";
7329
+ console.log(` ${s.id}${pts}${epic} \u2014 ${s.title} [${s.status}]`);
7330
+ }
7331
+ }
7332
+ } else if (subcommand === "done") {
7333
+ const id = rest[0];
7334
+ const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
7335
+ if (!id) {
7336
+ console.error("Usage: fh story done <US-N> [--points N]");
7337
+ process.exit(1);
7338
+ }
7339
+ if (pointsArg) updateStoryPoints(storiesDir, id, pointsArg);
7340
+ updateStoryStatus(storiesDir, id, "done");
7341
+ console.log(`\u2714 ${id} als done markiert`);
7342
+ } else if (subcommand === "sprint") {
7343
+ const id = rest[0];
7344
+ if (!id) {
7345
+ console.error("Usage: fh story sprint <US-N>");
7346
+ process.exit(1);
7347
+ }
7348
+ updateStoryStatus(storiesDir, id, "in-sprint");
7349
+ console.log(`\u2714 ${id} in Sprint gezogen`);
7350
+ } else if (subcommand === "show") {
7351
+ const id = rest[0];
7352
+ if (!id) {
7353
+ console.error("Usage: fh story show <US-N>");
7354
+ process.exit(1);
7355
+ }
7356
+ const story = getStory(storiesDir, id);
7357
+ if (!story) {
7358
+ console.error(`Story ${id} nicht gefunden`);
7359
+ process.exit(1);
7360
+ }
7361
+ console.log(formatStoryCard(story));
7362
+ } else {
7363
+ console.error("Verf\xFCgbar: fh story create | list | show | done");
7364
+ }
7365
+ } else if (command === "epic") {
7366
+ const epicsDir = path31.join(forgehiveDir, "memory", "epics");
7367
+ const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7368
+ if (subcommand === "create") {
7369
+ const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7370
+ const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
7371
+ if (!title) {
7372
+ console.error("Usage: fh epic create <titel> [--goal <ziel>]");
7373
+ process.exit(1);
7374
+ }
7375
+ const epic = createEpic(epicsDir, title, goalArg);
7376
+ console.log(`\u2714 ${epic.id} erstellt: ${path31.join(epicsDir, epic.id + ".md")}`);
7377
+ } else if (subcommand === "list") {
7378
+ const epics = listEpics(epicsDir);
7379
+ if (epics.length === 0) {
7380
+ console.log("Keine Epics gefunden.");
7381
+ } else {
7382
+ for (const e of epics)
7383
+ console.log(` ${e.id} \u2014 ${e.title} [${e.status}] (${e.stories.length} Stories)`);
7384
+ }
7385
+ } else if (subcommand === "show") {
7386
+ const id = rest[0];
7387
+ if (!id) {
7388
+ console.error("Usage: fh epic show <EPC-N>");
7389
+ process.exit(1);
7390
+ }
7391
+ const epic = getEpic(epicsDir, id);
7392
+ if (!epic) {
7393
+ console.error(`Epic ${id} nicht gefunden`);
7394
+ process.exit(1);
7395
+ }
7396
+ const stories = listStories(storiesDir).filter((s) => s.epicId === id);
7397
+ console.log(formatEpicCard(epic, stories));
7398
+ } else {
7399
+ console.error("Verf\xFCgbar: fh epic create | list | show");
7400
+ }
7401
+ } else if (command === "velocity") {
7402
+ const velocityFile = path31.join(forgehiveDir, "memory", "velocity.md");
7403
+ if (subcommand === "record") {
7404
+ const sprintNum = parseInt(rest[0] ?? "0", 10);
7405
+ const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
7406
+ const delivered = rest.includes("--delivered") ? parseInt(rest[rest.indexOf("--delivered") + 1], 10) : NaN;
7407
+ if (!sprintNum || isNaN(committed) || isNaN(delivered)) {
7408
+ console.error("Usage: fh velocity record <sprint-num> --committed N --delivered N");
7409
+ process.exit(1);
7410
+ }
7411
+ recordVelocity(velocityFile, sprintNum, committed, delivered);
7412
+ const avg = getRollingAverage(getVelocityHistory(velocityFile));
7413
+ console.log(`\u2714 Sprint ${sprintNum} gespeichert. Rolling Average: ${avg} Punkte`);
7414
+ } else if (!subcommand || subcommand === "show") {
7415
+ const history = getVelocityHistory(velocityFile);
7416
+ console.log(formatVelocityReport(history));
7417
+ } else {
7418
+ console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
7419
+ }
6972
7420
  } else {
6973
- const cmd = [command, subcommand].filter(Boolean).join(" ") || "(kein)";
6974
- console.error(`Unbekannter Befehl: ${cmd}`);
6975
- console.error("Verf\xFCgbar: init | confirm | rollback | scan --update | scan --check | status | ci [--format json|markdown] [--fail-on critical|high|any] [--init] | map | onboard [--output path] | changelog [--since tag] | metrics [--since date] | sync [push|pull] [--remote origin --branch forgehive-memory] | run <issue-url> [--agent <name>] | cost [today|week|all] | cost --limit N --alert N | memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | wire <service> | mcp [auth|search|add] | security [scan|deps|report|audit|permissions]");
7421
+ console.error("Unbekannter Befehl: " + command);
7422
+ console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | cost | memory | skills | party | wire | mcp | security");
7423
+ console.error("Hilfe: fh --help");
6976
7424
  process.exit(1);
6977
7425
  }
6978
7426
  /*! Bundled license information:
@@ -7,11 +7,15 @@ You are running a Sprint Planning session using the ForgeHive workflow.
7
7
  1. Read `.forgehive/capabilities.yaml` — understand the tech stack and constraints
8
8
  2. Read `.forgehive/memory/MEMORY.md` and all linked memory files — load project context
9
9
  3. Check if `.forgehive/memory/sprint.md` exists — if so, show the last sprint summary first
10
- 4. Run `fh scan --check` to verify the codebase snapshot is current
10
+ 4. Check if `.forgehive/memory/velocity.md` exists if so, show rolling average as capacity hint
11
+ 5. Run `fh scan --check` to verify the codebase snapshot is current
11
12
 
12
13
  ### Step 2: Collect backlog items
13
14
 
14
- Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — ein Item pro Zeile. Oder soll ich Linear/Jira laden?"**
15
+ Ask the user: **"Welche Items kommen in den Sprint? Liste sie auf — oder soll ich Backlog-Stories laden?"**
16
+
17
+ **If stories exist** in `.forgehive/memory/stories/` with status `backlog`:
18
+ Run `fh story list` to show available stories. Ask: **"Welche dieser Stories kommen in den Sprint?"**
15
19
 
16
20
  **If Linear MCP is available** (check if `.mcp.json` contains `linear`):
17
21
  Use the Linear MCP tool to fetch open issues:
@@ -25,7 +29,7 @@ Show the fetched issues and ask: **"Welche davon kommen in den Sprint?"**
25
29
  gh issue list --state open --label "sprint-candidate" --limit 20
26
30
  ```
27
31
 
28
- **If no MCP connected:**
32
+ **If no stories/MCP:**
29
33
  Accept free-text input — one item per line.
30
34
 
31
35
  ### Step 3: Clarify and refine
@@ -37,44 +41,59 @@ For each item, ask one clarifying question if needed:
37
41
 
38
42
  Do NOT ask more than one question per item. If the item is clear, skip this step.
39
43
 
40
- ### Step 4: Estimate effort
44
+ ### Step 4: Estimate with Fibonacci Points
41
45
 
42
- Estimate each item using T-shirt sizes:
46
+ Estimate each item using Fibonacci story points:
43
47
 
44
- | Size | Meaning | Typical scope |
48
+ | Points | Meaning | Typical scope |
45
49
  |---|---|---|
46
- | XS | < 2h | Config change, copy fix, small bug |
47
- | S | half day | Simple feature, isolated fix |
48
- | M | 1–2 days | Feature with tests, moderate complexity |
49
- | L | 3–5 days | Complex feature, multiple files, integration work |
50
- | XL | > 5 days | Should be broken down further |
50
+ | 1 | Trivial | Config change, copy fix, 1-line fix |
51
+ | 2 | Small | Simple isolated change |
52
+ | 3 | Medium-small | Small feature, isolated fix |
53
+ | 5 | Medium | Feature with tests, moderate complexity |
54
+ | 8 | Large | Complex feature, multiple files |
55
+ | 13 | Extra-large | Should be broken down |
56
+
57
+ **T-Shirt aliases:** XS=1, S=2, M=5, L=8, XL=13
51
58
 
52
- Flag any XL items: **"Dieses Item ist zu groß für einen Sprint — soll ich es aufteilen?"**
59
+ Flag any 13-point items: **"Dieses Item ist zu groß für einen Sprint — soll ich es aufteilen?"**
60
+
61
+ If items were loaded from `.forgehive/memory/stories/`, update their points:
62
+ ```bash
63
+ fh story <US-N> --points <N>
64
+ ```
53
65
 
54
66
  ### Step 5: Prioritize
55
67
 
56
68
  Sort items into three buckets:
57
69
 
58
- **Must (Sprint-Ziel)** — delivers the sprint goal, blocks other work, or is overdue
59
- **Should (Best Effort)** — important but not blocking
60
- **Could (Nice to Have)** — do if capacity allows
70
+ **Must (Sprint-Ziel)** — delivers the sprint goal, blocks other work, or is overdue
71
+ **Should (Best Effort)** — important but not blocking
72
+ **Could (Nice to Have)** — do if capacity allows
61
73
 
62
74
  Suggest a sprint goal in one sentence based on the Must items.
63
75
 
64
76
  ### Step 6: Check capacity
65
77
 
66
- Ask: **"Wie viele Entwickler-Tage habt ihr im Sprint?"** (default: 10 days for a 2-week sprint with one developer)
78
+ Show velocity hint if available:
79
+ ```bash
80
+ fh velocity show
81
+ ```
82
+
83
+ Ask: **"Wie viele Punkte habt ihr im Sprint?"**
67
84
 
68
- Calculate if the Must + Should items fit. If they don't, move the lowest-priority Should items to Could.
85
+ Default: rolling average from velocity history, or 20 points for a 2-week sprint with one developer.
86
+
87
+ Calculate if Must + Should items fit. If they don't, move the lowest-priority Should items to Could.
69
88
 
70
89
  Show a capacity summary:
71
90
  ```
72
- Sprint-Kapazität: 10 Tage
73
- Must: [sum] Tage
74
- Should: [sum] Tage
75
- Could: [sum] Tage
76
- ─────────────────
77
- Geplant: [must+should] / 10 Tage
91
+ Sprint-Kapazität: 20 Punkte
92
+ Must: [sum] Punkte
93
+ Should: [sum] Punkte
94
+ Could: [sum] Punkte
95
+ ─────────────────────
96
+ Geplant: [must+should] / 20 Punkte
78
97
  ```
79
98
 
80
99
  ### Step 7: Output the sprint plan
@@ -86,16 +105,16 @@ Write the sprint plan to `.forgehive/memory/sprint.md` in this format:
86
105
 
87
106
  **Ziel:** [one sentence sprint goal]
88
107
 
89
- **Kapazität:** [X] Entwickler-Tage
108
+ **Kapazität:** [X] Punkte
90
109
 
91
110
  ## Must
92
- - [ ] [Item] ([size]) — [one-line description]
111
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
93
112
 
94
- ## Should
95
- - [ ] [Item] ([size]) — [one-line description]
113
+ ## Should
114
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
96
115
 
97
116
  ## Could
98
- - [ ] [Item] ([size]) — [one-line description]
117
+ - [ ] [ID] [Item] ([points]pt) — [one-line description]
99
118
 
100
119
  ## Offen / Blocked
101
120
  - [any blocked items with reason]
@@ -104,10 +123,25 @@ Write the sprint plan to `.forgehive/memory/sprint.md` in this format:
104
123
  *Erstellt mit fh sprint — [timestamp]*
105
124
  ```
106
125
 
107
- Confirm with the user: **"Sprint Plan gespeichert in `.forgehive/memory/sprint.md`. Soll ich für jedes Must-Item direkt einen Branch anlegen?"**
126
+ Confirm with the user: **"Sprint Plan gespeichert. Soll ich für jedes Must-Item direkt einen Branch anlegen?"**
108
127
 
109
- If yes: create branches for each Must item following the `git-conventions` skill (`feat/<slug>`, `fix/<slug>`, `chore/<slug>`).
128
+ If yes: create branches following `feat/<slug>`, `fix/<slug>`, `chore/<slug>`.
110
129
 
111
130
  ### Step 8: Update project memory
112
131
 
113
- Append the sprint goal to `.forgehive/memory/project.md` under a `## Aktueller Sprint` section so Claude knows the current focus in every future session.
132
+ Append the sprint goal to `.forgehive/memory/project.md` under a `## Aktueller Sprint` section.
133
+
134
+ ### Step 9: Record velocity (at sprint end)
135
+
136
+ When the user runs `/fh-sprint` and mentions "Sprint ist fertig" or "Sprint abgeschlossen":
137
+
138
+ 1. Ask: **"Wie viele Punkte habt ihr tatsächlich geliefert?"**
139
+ 2. Read the committed points from `sprint.md`
140
+ 3. Record velocity:
141
+ ```bash
142
+ fh velocity record <N> --committed <committed> --delivered <delivered>
143
+ ```
144
+ 4. Show updated velocity report:
145
+ ```bash
146
+ fh velocity show
147
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {