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 +262 -33
- package/dist/cli.js +504 -56
- package/forgehive/commands/fh-sprint.md +65 -31
- package/package.json +1 -1
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-
|
|
18
|
-
<img src="https://img.shields.io/badge/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:
|
|
48
|
+
## Status: v0.7 — Stable
|
|
44
49
|
|
|
45
|
-
|
|
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 (~
|
|
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.
|
|
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
|
-
**
|
|
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
|
|
426
|
-
scan-result.yaml
|
|
427
|
-
.scan-hash
|
|
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
|
|
430
|
-
constraints.yaml
|
|
431
|
-
permissions.yaml
|
|
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
|
|
434
|
-
project.md
|
|
435
|
-
feedback.md
|
|
436
|
-
stack.md
|
|
437
|
-
adrs/
|
|
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/
|
|
440
|
-
expert/
|
|
441
|
-
workflows/
|
|
442
|
-
worktrees/
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
|
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
|
-
#
|
|
559
|
-
#
|
|
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
|
|
845
|
+
| Build | esbuild -> `dist/cli.js` (~244 KB, single bundle) |
|
|
617
846
|
| Type check | `tsc --noEmit` |
|
|
618
|
-
| Tests | `node:test` (native) + tsx ESM loader,
|
|
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
|
|
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
|
|
2755
|
-
import
|
|
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 =
|
|
6460
|
+
var forgehiveDir = path31.join(projectRoot, ".forgehive");
|
|
6203
6461
|
if (command === "--version" || command === "-v") {
|
|
6204
|
-
console.log("0.7.
|
|
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 =
|
|
6209
|
-
|
|
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 (!
|
|
6216
|
-
return
|
|
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
|
-
|
|
6231
|
-
const runtimeDir =
|
|
6232
|
-
|
|
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 (!
|
|
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] ??
|
|
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] ??
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
6854
|
-
|
|
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 =
|
|
6867
|
-
console.log(
|
|
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 =
|
|
6880
|
-
|
|
6881
|
-
const outPath =
|
|
6882
|
-
|
|
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
|
-
|
|
6890
|
-
|
|
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 =
|
|
6898
|
-
|
|
6899
|
-
|
|
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 ??
|
|
7238
|
+
const outputPath = outputArg ?? path31.join(projectRoot, "ONBOARDING.md");
|
|
6906
7239
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
6907
|
-
|
|
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 =
|
|
6918
|
-
if (
|
|
6919
|
-
const pkg = JSON.parse(
|
|
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 ??
|
|
7258
|
+
const outputPath = outputArg ?? path31.join(projectRoot, "CHANGELOG.md");
|
|
6926
7259
|
let existing = "";
|
|
6927
|
-
if (
|
|
6928
|
-
|
|
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,
|
|
6936
|
-
const metricsPath =
|
|
6937
|
-
|
|
6938
|
-
|
|
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
|
-
|
|
6974
|
-
console.error(
|
|
6975
|
-
console.error("
|
|
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.
|
|
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 —
|
|
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
|
|
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
|
|
44
|
+
### Step 4: Estimate with Fibonacci Points
|
|
41
45
|
|
|
42
|
-
Estimate each item using
|
|
46
|
+
Estimate each item using Fibonacci story points:
|
|
43
47
|
|
|
44
|
-
|
|
|
48
|
+
| Points | Meaning | Typical scope |
|
|
45
49
|
|---|---|---|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
73
|
-
Must: [sum]
|
|
74
|
-
Should: [sum]
|
|
75
|
-
Could: [sum]
|
|
76
|
-
|
|
77
|
-
Geplant: [must+should] /
|
|
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]
|
|
108
|
+
**Kapazität:** [X] Punkte
|
|
90
109
|
|
|
91
110
|
## Must
|
|
92
|
-
- [ ] [Item] ([
|
|
111
|
+
- [ ] [ID] [Item] ([points]pt) — [one-line description]
|
|
93
112
|
|
|
94
|
-
## Should
|
|
95
|
-
- [ ] [Item] ([
|
|
113
|
+
## Should
|
|
114
|
+
- [ ] [ID] [Item] ([points]pt) — [one-line description]
|
|
96
115
|
|
|
97
116
|
## Could
|
|
98
|
-
- [ ] [Item] ([
|
|
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
|
|
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
|
|
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
|
|
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
|
+
```
|