forgedock 1.0.7 → 1.0.9

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.
@@ -0,0 +1,172 @@
1
+ # Intelligent, Idiot-Proof Onboarding — Design Spec
2
+
3
+ **Date:** 2026-06-09
4
+ **Status:** Approved (design); pending implementation plan
5
+ **Scope:** One milestone, four phases
6
+
7
+ ## Problem
8
+
9
+ Today's onboarding (`npx forgedock` TUI + `forgedock init` + the `forgedock-init`
10
+ skill) works but asks the first-time user ~12 questions across project / paths /
11
+ branches / optional sections, with little explanation of *why* each is asked or
12
+ *what* a good answer looks like. The deterministic auto-detect is decent, but the
13
+ genuinely hard sections (project board field mapping, satellites, review context,
14
+ verification endpoints) still fall back to manual prompts. There is no per-directory
15
+ control: ForgeDock commands install globally and there is no notion of "activate
16
+ here / stay silent there."
17
+
18
+ We want onboarding to be **intelligent** (AI fills the config), **idiot-proof**
19
+ (near-zero questions, everything explained), and **scoped** (a Claude Code hook
20
+ that activates ForgeDock only in directories the user has opted into).
21
+
22
+ ## Vision
23
+
24
+ Open `claude` in any repo → a SessionStart hook resolves the directory's ForgeDock
25
+ state → if managed and active and unconfigured, an AI agent infers the **entire**
26
+ `forge.yaml` from the codebase + GitHub → the user reviews **one annotated screen**
27
+ and presses Enter. No wizard. Per-directory enable/disable means ForgeDock only
28
+ wakes up where wanted.
29
+
30
+ ## Architecture — three pillars
31
+
32
+ ### Pillar A — Autopilot config generation (the "intelligence")
33
+
34
+ Layered backends behind **one interface** so the experience degrades gracefully
35
+ (the chosen "hybrid" model):
36
+
37
+ - **`init-detect`** — deterministic, no AI. Refactors today's git / tech-stack /
38
+ board detection into a pure module that returns a `ConfigDraft` plus a
39
+ **confidence score per field** (`high` = verified from a concrete source,
40
+ `medium` = inferred, `low` = guessed default).
41
+ - **`init-enrich`** — AI enrichment. Consumes a `ConfigDraft`, deeply infers the
42
+ hard sections, and raises confidences. Two interchangeable backends sharing one
43
+ contract:
44
+ - **skill backend** — runs inside Claude Code; no API key; uses existing CC auth.
45
+ *Primary path.*
46
+ - **api backend** — the CLI calls the Anthropic API directly with a BYO
47
+ `ANTHROPIC_API_KEY`. *Used when Claude Code is not present.*
48
+ - **`review-render`** — a single annotated review screen. Every field is shown with
49
+ its **source**, **confidence**, and a plain-language "why this value." Enter to
50
+ accept all; inline-edit to change any field. **Unknowable items get a sensible
51
+ default plus an inline `# TODO(forgedock:<field>)` flag in the YAML — never a
52
+ blocking prompt.**
53
+
54
+ **Backend selection logic:**
55
+ 1. Always run `init-detect` to produce the baseline draft.
56
+ 2. If running inside Claude Code → `init-enrich` via **skill** backend.
57
+ 3. Else if `ANTHROPIC_API_KEY` present → `init-enrich` via **api** backend.
58
+ 4. Else → skip enrichment; render the deterministic baseline (today's quality, but
59
+ through the new annotated review screen).
60
+
61
+ ### Pillar B — Claude Code hook + per-directory toggle
62
+
63
+ - **SessionStart hook**, installed into `~/.claude/settings.json` by
64
+ `forgedock install`. On each CC session it resolves the cwd's state and acts:
65
+ - **managed + active** → inject ForgeDock context (a `forge.yaml` summary +
66
+ available commands); if config is missing or stale, offer a one-shot autopilot
67
+ init.
68
+ - **managed + opted-out** → completely silent (no output, no context injection).
69
+ - **unmanaged** → an optional, one-time, suppressible "Enable ForgeDock here?"
70
+ nudge.
71
+ - **State model** ("marker + global opt-out"):
72
+ - A directory is **managed** iff it contains `forge.yaml` **or** a lightweight
73
+ `.forgedock` marker (lets a project signal "ForgeDock-managed" before a full
74
+ config exists).
75
+ - A central `~/.claude/forgedock/registry.json` tracks explicit **per-directory
76
+ opt-out** plus last-seen metadata. Opt-out wins over the marker.
77
+ - **New CLI commands:** `forgedock enable [dir]`, `forgedock disable [dir]`,
78
+ `forgedock status [dir]`.
79
+ - **Known constraint (explicit, accepted):** Claude Code slash-commands install
80
+ **globally**, so "disable per dir" means the **hook stays silent and injects
81
+ nothing** in that directory — it does not physically remove or hide the global
82
+ commands. This is the achievable and intended semantics.
83
+
84
+ ### Pillar C — Idiot-proofing (cross-cutting)
85
+
86
+ - **Self-healing `validate`** — on a validation failure, the AI proposes (and, with
87
+ confirmation, applies) the fix instead of only printing an error.
88
+ - **Actionable preflight** — failures (`gh` missing, not authed, Node too old) show
89
+ copy-paste fix commands (partially exists today; standardize it).
90
+ - **Escape hatch** — `--manual` / `--verbose` runs the full guided wizard for power
91
+ users who *want* every prompt.
92
+
93
+ ## Components & interfaces (designed for isolation)
94
+
95
+ | Unit | Purpose | Depends on | Testable in isolation? |
96
+ |------|---------|-----------|------------------------|
97
+ | `init-detect` | Pure baseline `ConfigDraft` + per-field confidence | git, fs (read-only) | Yes — fixture repos → expected draft |
98
+ | `init-enrich` (skill) | AI enrichment inside CC | CC session, draft contract | Contract test + recorded fixture |
99
+ | `init-enrich` (api) | AI enrichment via Anthropic API | `ANTHROPIC_API_KEY`, draft contract | Contract test against same interface |
100
+ | `review-render` | Annotated review/accept/edit TUI | draft + confidence, tui.mjs | Yes — render snapshot from a draft |
101
+ | `registry` | Resolve dir state; read/write opt-out + markers | fs | Yes — state-resolution matrix |
102
+ | `session-hook` | Thin SessionStart script; emits context JSON | registry | Yes — simulated SessionStart payloads |
103
+
104
+ **`ConfigDraft` contract** (shared by detect → enrich → render): a structured
105
+ object mirroring `forge.yaml` sections, where each leaf is `{ value, confidence,
106
+ source, why }`. This is the single seam that lets the three enrichment backends and
107
+ the renderer stay decoupled.
108
+
109
+ ## Data flow (autopilot happy path)
110
+
111
+ ```
112
+ open `claude` in repo
113
+ → SessionStart hook fires
114
+ → registry resolves: managed (forge.yaml absent) + active
115
+ → hook suggests autopilot init (or user runs `forgedock init`)
116
+ → init-detect → baseline ConfigDraft + confidences
117
+ → init-enrich (skill) → enriched ConfigDraft
118
+ → review-render → annotated screen → Enter to accept
119
+ → write forge.yaml (with any # TODO(forgedock:…) flags)
120
+ → validate (self-healing on failure)
121
+ ```
122
+
123
+ ## Error handling
124
+
125
+ - **No git remote / detached repo:** `init-detect` lowers confidence, leaves fields
126
+ as `low`-confidence defaults with TODO flags; never crashes.
127
+ - **`gh` unauthenticated:** GitHub-derived sections (board, satellites) are skipped
128
+ with a flagged TODO and a copy-paste `gh auth login` hint.
129
+ - **Enrichment backend unavailable / API error:** fall through the backend-selection
130
+ ladder; worst case is the deterministic baseline through the review screen.
131
+ - **`forge.yaml` already exists:** existing backup-and-overwrite behavior is
132
+ preserved; review screen shows a diff-style "changed vs current."
133
+ - **Registry file missing/corrupt:** treat as empty opt-out set; the hook fails open
134
+ to silent (never blocks a CC session).
135
+
136
+ ## Testing strategy
137
+
138
+ - **`registry`:** unit-test the full state matrix — managed/unmanaged × opted-in/out
139
+ × `forge.yaml` present/absent.
140
+ - **`init-detect`:** fixture repos (node, python, rust, multi-repo) → expected
141
+ `ConfigDraft` snapshots.
142
+ - **`session-hook`:** feed simulated SessionStart payloads; assert silent vs
143
+ context-injecting vs nudge output.
144
+ - **`init-enrich`:** test the `ConfigDraft` contract both backends must satisfy;
145
+ pin one recorded enrichment fixture for regression.
146
+ - **`review-render`:** snapshot the annotated screen from a known draft.
147
+
148
+ ## Phasing (milestone → issues)
149
+
150
+ 1. **Foundation** — `registry` module + `enable`/`disable`/`status` commands +
151
+ marker handling. Unblocks the hook.
152
+ 2. **Hook** — SessionStart hook install into `settings.json` + context injection +
153
+ opt-out respect + unmanaged nudge.
154
+ 3. **Autopilot brain** — `init-detect` refactor (`ConfigDraft` + confidence) →
155
+ `init-enrich` skill upgrade → annotated `review-render` screen.
156
+ 4. **Resilience** — BYO-key `api` enrichment backend + self-healing `validate` +
157
+ `--manual` escape hatch.
158
+
159
+ All four phases ship in **one milestone**.
160
+
161
+ ## Out of scope / non-goals
162
+
163
+ - Physically hiding global slash-commands per directory (see Pillar B constraint).
164
+ - A standalone web onboarding UI.
165
+ - Multi-user / team-shared registries (registry is per-machine, user-level).
166
+
167
+ ## Open questions
168
+
169
+ - Exact confidence thresholds that trigger a TODO flag vs a silent default
170
+ (tune during Phase 3).
171
+ - Whether the unmanaged-directory nudge defaults to on or off (lean: off, opt-in via
172
+ a global setting) — decide in Phase 2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgedock",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "GitHub as a knowledge graph for AI agents. Autonomous dev pipeline for Claude Code.",
5
5
  "bin": {
6
6
  "forgedock": "bin/forgedock.mjs"
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "files": [
35
35
  "bin/",
36
+ "!bin/tests/",
36
37
  "commands/",
37
38
  "docs/",
38
39
  "scripts/",
@@ -40,6 +41,9 @@
40
41
  "LICENSE",
41
42
  "README.md"
42
43
  ],
44
+ "scripts": {
45
+ "test": "node --test \"bin/tests/**/*.test.mjs\""
46
+ },
43
47
  "engines": {
44
48
  "node": ">=18"
45
49
  }
@@ -49,27 +49,53 @@ WARNINGS=0
49
49
 
50
50
  # --- Next.js Route Handler registration check ---
51
51
  while read -r f; do
52
- URL_PATH=$(echo "$f" | sed "s|^${FORGE_PAGES_ROOT}||; s|/route\\.ts\$||; s|\\[.*\\]|*|g")
52
+ # Use shell parameter expansion instead of sed to avoid interpreting FORGE_PAGES_ROOT
53
+ # as a sed pattern (values containing | or other metacharacters break the substitution).
54
+ stripped="${f#${FORGE_PAGES_ROOT}/}"
55
+ stripped="${stripped%/route.ts}"
56
+ # Replace dynamic segments [param] with * using a loop (no regex needed)
57
+ URL_PATH=""
58
+ IFS='/' read -ra SEGMENTS <<< "$stripped"
59
+ for seg in "${SEGMENTS[@]}"; do
60
+ case "$seg" in
61
+ \[*\]) URL_PATH="${URL_PATH}/*" ;;
62
+ *) URL_PATH="${URL_PATH}/${seg}" ;;
63
+ esac
64
+ done
53
65
  ROUTE_SEGMENT=$(echo "$URL_PATH" | sed 's|/api/v1/||; s|/.*||')
54
66
 
55
- # Check if next.config.js has a rewrite that might shadow this route
56
- if [ -f "$REPO_ROOT/web/next.config.js" ]; then
57
- if grep -q "$ROUTE_SEGMENT" "$REPO_ROOT/web/next.config.js" 2>/dev/null; then
58
- echo "WARNING: Route handler $f ($URL_PATH) — next.config.js references '$ROUTE_SEGMENT', may shadow this route"
59
- WARNINGS=$((WARNINGS + 1))
60
- else
61
- echo "OK: Route handler $f ($URL_PATH) — no shadowing rewrites found in next.config.js"
67
+ # Guard: skip shadow checks when ROUTE_SEGMENT is empty (e.g. /api/ root handlers).
68
+ # An empty ROUTE_SEGMENT causes grep -qF "" to match every line — false-positive WARNINGs.
69
+ if [ -z "$ROUTE_SEGMENT" ]; then
70
+ echo "OK: Route handler $f ($URL_PATH) — root handler, no segment to match, skipping shadow checks"
71
+ else
72
+ # Check if next.config.js has a rewrite that might shadow this route
73
+ if [ -f "$REPO_ROOT/web/next.config.js" ]; then
74
+ # Use -F (fixed string) so ROUTE_SEGMENT is not interpreted as a regex pattern
75
+ if grep -qF "$ROUTE_SEGMENT" "$REPO_ROOT/web/next.config.js" 2>/dev/null; then
76
+ echo "WARNING: Route handler $f ($URL_PATH) — next.config.js references '$ROUTE_SEGMENT', may shadow this route"
77
+ WARNINGS=$((WARNINGS + 1))
78
+ else
79
+ echo "OK: Route handler $f ($URL_PATH) — no shadowing rewrites found in next.config.js"
80
+ fi
62
81
  fi
63
- fi
64
82
 
65
- # Check if nginx routes this path to Next.js or directly to backend
66
- if [ -f "$REPO_ROOT/infra/nginx/nginx.conf" ]; then
67
- if grep -q "location.*$ROUTE_SEGMENT" "$REPO_ROOT/infra/nginx/nginx.conf" 2>/dev/null; then
68
- echo "WARNING: Route handler $f ($URL_PATH) — nginx.conf has a location block for '$ROUTE_SEGMENT', may bypass Next.js"
69
- WARNINGS=$((WARNINGS + 1))
83
+ # Check if nginx routes this path to Next.js or directly to backend
84
+ if [ -f "$REPO_ROOT/infra/nginx/nginx.conf" ]; then
85
+ # Check for location blocks mentioning ROUTE_SEGMENT — use two -F searches to avoid
86
+ # embedding the variable in a regex: first find lines with "location", then filter
87
+ # for the literal segment. This avoids ROUTE_SEGMENT being interpreted as a BRE.
88
+ if grep -F "location" "$REPO_ROOT/infra/nginx/nginx.conf" 2>/dev/null | grep -qF "$ROUTE_SEGMENT"; then
89
+ echo "WARNING: Route handler $f ($URL_PATH) — nginx.conf has a location block for '$ROUTE_SEGMENT', may bypass Next.js"
90
+ WARNINGS=$((WARNINGS + 1))
91
+ fi
70
92
  fi
71
93
  fi
72
- done < <(echo "$FILES" | grep -E "^${FORGE_PAGES_ROOT}/api/.*route\\.ts$") || true
94
+ done < <(echo "$FILES" | while IFS= read -r line; do
95
+ case "$line" in
96
+ "${FORGE_PAGES_ROOT}/api/"*"route.ts") echo "$line" ;;
97
+ esac
98
+ done) || true
73
99
 
74
100
  # --- Python API Router registration check ---
75
101
  while read -r f; do
@@ -77,7 +103,8 @@ while read -r f; do
77
103
  MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
78
104
 
79
105
  if [ -f "$MAIN_PY" ]; then
80
- if grep -q "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
106
+ # Use -F (fixed string) so ROUTER_NAME is not interpreted as a regex pattern
107
+ if grep -qF "$ROUTER_NAME" "$MAIN_PY" 2>/dev/null; then
81
108
  echo "OK: API Router '$ROUTER_NAME' ($f) — registered in $(basename "$MAIN_PY")"
82
109
  else
83
110
  echo "BLOCKING: API Router '$ROUTER_NAME' ($f) — NOT found in $(basename "$MAIN_PY"). Router will not be reachable."
@@ -95,7 +122,8 @@ while read -r f; do
95
122
  MAIN_PY="$REPO_ROOT/${FORGE_API_MAIN}"
96
123
 
97
124
  if [ -f "$MAIN_PY" ]; then
98
- if grep -q "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
125
+ # Use -F (fixed string) so MIDDLEWARE_NAME is not interpreted as a regex pattern
126
+ if grep -qF "$MIDDLEWARE_NAME" "$MAIN_PY" 2>/dev/null; then
99
127
  echo "OK: Middleware '$MIDDLEWARE_NAME' ($f) — registered in $(basename "$MAIN_PY")"
100
128
  else
101
129
  echo "WARNING: Middleware '$MIDDLEWARE_NAME' ($f) — not found in $(basename "$MAIN_PY"). May not be active."
@@ -108,7 +136,8 @@ done < <(echo "$FILES" | grep -E "^${FORGE_API_MIDDLEWARE_DIR}/.*\\.py$") || tru
108
136
  while read -r f; do
109
137
  MODULE_NAME=$(basename "$f" .py)
110
138
 
111
- IMPORTERS=$(grep -rl "$MODULE_NAME" "$REPO_ROOT/${FORGE_API_SERVICES_DIR}/" "$REPO_ROOT/${FORGE_WORKER_DIR}/" 2>/dev/null | head -5)
139
+ # Use -F (fixed string) so MODULE_NAME is not interpreted as a regex pattern
140
+ IMPORTERS=$(grep -rlF "$MODULE_NAME" "$REPO_ROOT/${FORGE_API_SERVICES_DIR}/" "$REPO_ROOT/${FORGE_WORKER_DIR}/" 2>/dev/null | head -5)
112
141
  if [ -n "$IMPORTERS" ]; then
113
142
  echo "OK: Shared module '$MODULE_NAME' ($f) — imported by services"
114
143
  else
@@ -121,7 +150,9 @@ done < <(echo "$FILES" | grep -E "^shared/.*\.py$") || true
121
150
  while read -r f; do
122
151
  COMPONENT_NAME=$(basename "$f" .tsx)
123
152
 
124
- IMPORTERS=$(grep -rl "$COMPONENT_NAME" "$REPO_ROOT/${FORGE_PAGES_ROOT}/" "$REPO_ROOT/${FORGE_COMPONENTS_DIR}/" 2>/dev/null | grep -v "$f" | head -3)
153
+ # Use -F (fixed string) for both the component name search and the self-exclusion
154
+ # to prevent file paths and component names from being interpreted as regex patterns
155
+ IMPORTERS=$(grep -rlF "$COMPONENT_NAME" "$REPO_ROOT/${FORGE_PAGES_ROOT}/" "$REPO_ROOT/${FORGE_COMPONENTS_DIR}/" 2>/dev/null | grep -vF "$f" | head -3)
125
156
  if [ -n "$IMPORTERS" ]; then
126
157
  echo "OK: Component '$COMPONENT_NAME' ($f) — imported by other files"
127
158
  else
@@ -80,7 +80,9 @@ if [ -n "$NEW_MAPPINGS" ]; then
80
80
 
81
81
  # For each new mapping, verify the SOPS path structure exists
82
82
  # (We can't decrypt SOPS to verify values, but we can check the mapping format is valid)
83
- echo "$NEW_MAPPINGS" | while IFS= read -r mapping; do
83
+ # Use process substitution instead of a pipe so WARNINGS increments persist in the parent shell.
84
+ # (echo ... | while read creates a subshell — counter changes are lost on subshell exit.)
85
+ while IFS= read -r mapping; do
84
86
  VAR_NAME=$(echo "$mapping" | grep -oE '^"[A-Z_]+"' | tr -d '"')
85
87
  SOPS_SECTION=$(echo "$mapping" | grep -oE '\("([a-z_]+)"' | head -1 | tr -d '("')
86
88
  SOPS_KEY=$(echo "$mapping" | grep -oE ', "([a-z_]+)"\)' | tr -d ', ")')
@@ -91,7 +93,7 @@ if [ -n "$NEW_MAPPINGS" ]; then
91
93
  echo "WARNING: Malformed ENV_MAPPING entry: $mapping"
92
94
  WARNINGS=$((WARNINGS + 1))
93
95
  fi
94
- done
96
+ done < <(echo "$NEW_MAPPINGS")
95
97
  else
96
98
  echo "OK: No new ENV_MAPPING entries in diff"
97
99
  fi