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.
- package/bin/forge-utils.mjs +164 -0
- package/bin/forgedock.mjs +2054 -496
- package/bin/hooks/session-start.mjs +317 -0
- package/bin/init-detect.mjs +325 -0
- package/bin/init-enrich-api.mjs +203 -0
- package/bin/registry.mjs +358 -0
- package/bin/tui.mjs +453 -2
- package/commands/autopilot.md +6 -0
- package/commands/deploy-info.md +8 -8
- package/commands/forgedock-init.md +305 -384
- package/commands/orchestrate.md +3 -3
- package/commands/review-pr-staging.md +21 -21
- package/commands/review-pr.md +17 -17
- package/commands/rollback.md +2 -2
- package/commands/work-on/build/validate.md +4 -4
- package/commands/work-on-monolithic.md +18 -1
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +167 -0
- package/docs/superpowers/specs/2026-06-09-intelligent-onboarding-design.md +172 -0
- package/package.json +5 -1
- package/scripts/verify-route-registration.sh +50 -19
- package/scripts/verify-sops-chain.sh +4 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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" |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|