@jualopezmo/codeforge 0.4.0
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/LICENSE +21 -0
- package/README.md +368 -0
- package/VERSION +1 -0
- package/bin/codeforge.mjs +64 -0
- package/install.ps1 +317 -0
- package/install.sh +326 -0
- package/package.json +39 -0
- package/src/CLAUDE.md +88 -0
- package/src/CONTINUITY.template.md +16 -0
- package/src/PROJECT.template.md +27 -0
- package/src/configs/claude/settings.json +6 -0
- package/src/configs/codex/config.toml +17 -0
- package/src/configs/opencode.json +14 -0
- package/src/docs/CHANGELOG.md +9 -0
- package/src/docs/adr/.gitkeep +0 -0
- package/src/docs/e2e/reports/.gitkeep +0 -0
- package/src/docs/e2e/use-cases/.gitkeep +0 -0
- package/src/docs/extending.md +134 -0
- package/src/docs/plans/.gitkeep +0 -0
- package/src/docs/prds/.gitkeep +0 -0
- package/src/docs/research/.gitkeep +0 -0
- package/src/docs/solutions/.gitkeep +0 -0
- package/src/shared/rules/approach-comparison.md +29 -0
- package/src/shared/rules/continuity.md +32 -0
- package/src/shared/rules/docs-layout.md +23 -0
- package/src/shared/rules/memory.md +34 -0
- package/src/shared/rules/models.md +59 -0
- package/src/shared/rules/project-rules.md +31 -0
- package/src/shared/rules/research.md +30 -0
- package/src/shared/rules/severity.md +17 -0
- package/src/shared/rules/ship-gates.md +164 -0
- package/src/shared/rules/tdd.md +20 -0
- package/src/shared/rules/workflow.md +38 -0
- package/src/shared/scripts/check-gates.ps1 +197 -0
- package/src/shared/scripts/check-gates.sh +201 -0
- package/src/shared/scripts/claude-gate-hook.ps1 +35 -0
- package/src/shared/scripts/claude-gate-hook.sh +43 -0
- package/src/shared/state.template.md +33 -0
- package/src/skills/adr/SKILL.md +63 -0
- package/src/skills/checkpoint/SKILL.md +50 -0
- package/src/skills/council/SKILL.md +87 -0
- package/src/skills/finish-branch/SKILL.md +85 -0
- package/src/skills/fix-bug/SKILL.md +84 -0
- package/src/skills/index/SKILL.md +52 -0
- package/src/skills/new-feature/SKILL.md +84 -0
- package/src/skills/plan/SKILL.md +63 -0
- package/src/skills/prd/SKILL.md +62 -0
- package/src/skills/quick-fix/SKILL.md +60 -0
- package/src/skills/research/SKILL.md +61 -0
- package/src/skills/review/SKILL.md +66 -0
- package/src/skills/simplify/SKILL.md +59 -0
- package/src/skills/verify-e2e/SKILL.md +102 -0
- package/src/sync.ps1 +69 -0
- package/src/sync.sh +69 -0
package/install.ps1
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
#
|
|
3
|
+
# codeforge installer (Windows / PowerShell) — copy the workflow discipline into a target
|
|
4
|
+
# project. Mirror of install.sh.
|
|
5
|
+
#
|
|
6
|
+
# pwsh ./install.ps1 [target-dir] [-Upgrade] [-WithHooks] [-GitInit] [-NoIsolate]
|
|
7
|
+
#
|
|
8
|
+
# With no target-dir, installs into the current working directory.
|
|
9
|
+
#
|
|
10
|
+
# THIN INSTALL: the target receives only what the agent needs at RUNTIME. All framework
|
|
11
|
+
# machinery (the neutral source in ./src/, the generators sync.sh/ps1, the generation
|
|
12
|
+
# inputs in configs/, and the seed templates) stays in the codeforge repo — never copied
|
|
13
|
+
# into the target. To customize or upgrade, edit the codeforge source and re-run this
|
|
14
|
+
# installer against the target (-Upgrade, or a bare re-run from inside the project).
|
|
15
|
+
#
|
|
16
|
+
# This installer copies the runtime subset into the target, then runs `sync.ps1 -Out <target>`
|
|
17
|
+
# to GENERATE each engine's config + skills straight into the target. No symlinks. The
|
|
18
|
+
# generated engine artifacts are COMMITTED with the target so a fresh clone works with no
|
|
19
|
+
# codeforge dependency at runtime.
|
|
20
|
+
#
|
|
21
|
+
# LANDS IN THE TARGET (runtime only): CLAUDE.md, AGENTS.md, opencode.json, .claude/,
|
|
22
|
+
# .agents/, .codex/ (generated), shared/rules/*.md + shared/state.template.md + shared/scripts/* (managed),
|
|
23
|
+
# docs/ scaffolding + CHANGELOG, PROJECT.md + CONTINUITY.md (project-owned, seeded).
|
|
24
|
+
# STAYS IN codeforge (never copied): src/skills (neutral), configs/, sync.sh, sync.ps1,
|
|
25
|
+
# *.template.md, docs/extending.md.
|
|
26
|
+
#
|
|
27
|
+
param(
|
|
28
|
+
[Parameter(Mandatory = $false)][string]$Target,
|
|
29
|
+
[switch]$Upgrade,
|
|
30
|
+
[switch]$WithHooks,
|
|
31
|
+
[switch]$GitInit,
|
|
32
|
+
[switch]$NoIsolate
|
|
33
|
+
)
|
|
34
|
+
$ErrorActionPreference = 'Stop'
|
|
35
|
+
|
|
36
|
+
$Src = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
37
|
+
$Payload = Join-Path $Src 'src'
|
|
38
|
+
$forgeVersion = "unknown"
|
|
39
|
+
$versionFile = Join-Path $Src 'VERSION'
|
|
40
|
+
if (Test-Path -LiteralPath $versionFile -PathType Leaf) {
|
|
41
|
+
$v = (Get-Content -LiteralPath $versionFile -TotalCount 1)
|
|
42
|
+
if ($v) { $forgeVersion = $v.Trim() }
|
|
43
|
+
}
|
|
44
|
+
$Mode = if ($Upgrade) { 'upgrade' } else { 'install' }
|
|
45
|
+
if (-not $Target) { $Target = (Get-Location).Path }
|
|
46
|
+
|
|
47
|
+
if (-not (Test-Path -PathType Container $Target)) { Write-Error "target dir not found: $Target"; exit 2 }
|
|
48
|
+
$Target = (Resolve-Path $Target).Path
|
|
49
|
+
# Did a prior forge install own .claude/settings.local.json? (read before the manifest is rewritten)
|
|
50
|
+
$priorLocalManaged = $false
|
|
51
|
+
$mf = Join-Path $Target '.forge-manifest'
|
|
52
|
+
if ((Test-Path -LiteralPath $mf -PathType Leaf) -and (Select-String -LiteralPath $mf -Pattern '^localsettings:managed$' -Quiet)) {
|
|
53
|
+
$priorLocalManaged = $true
|
|
54
|
+
}
|
|
55
|
+
if (-not ((Test-Path (Join-Path $Payload 'CLAUDE.md')) -and (Test-Path (Join-Path $Payload 'skills')))) {
|
|
56
|
+
Write-Error "payload not found — run this from the codeforge repo"; exit 2
|
|
57
|
+
}
|
|
58
|
+
if ($Target -eq $Src) { Write-Error "refusing to install into codeforge itself"; exit 2 }
|
|
59
|
+
if ($Target -eq $Payload) { Write-Error "refusing to install into the codeforge payload dir (src/)"; exit 2 }
|
|
60
|
+
|
|
61
|
+
Write-Host "codeforge $forgeVersion -> installing into: $Target (mode: $Mode)"
|
|
62
|
+
|
|
63
|
+
# --- version drift advisory (informational only, never blocks) ---
|
|
64
|
+
$priorVersion = ""
|
|
65
|
+
$fvFile = Join-Path $Target '.forge-version'
|
|
66
|
+
if (Test-Path -LiteralPath $fvFile -PathType Leaf) {
|
|
67
|
+
$pv = (Get-Content -LiteralPath $fvFile -TotalCount 1)
|
|
68
|
+
if ($pv) { $priorVersion = $pv.Trim() }
|
|
69
|
+
}
|
|
70
|
+
if ($priorVersion -and $priorVersion -ne $forgeVersion -and $forgeVersion -ne 'unknown' -and $priorVersion -ne 'unknown') {
|
|
71
|
+
try { $isUpgrade = ([version]$priorVersion -lt [version]$forgeVersion) }
|
|
72
|
+
catch { $isUpgrade = ($priorVersion -lt $forgeVersion) }
|
|
73
|
+
if ($isUpgrade) {
|
|
74
|
+
Write-Host " ~ upgrading this target: codeforge $priorVersion -> $forgeVersion"
|
|
75
|
+
} else {
|
|
76
|
+
Write-Host " ! this target was installed by a NEWER codeforge ($priorVersion) than you're running ($forgeVersion)."
|
|
77
|
+
Write-Host " You may be downgrading it; teammates pinned to $priorVersion could see drift. (advisory only)"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Has-ForgeMarker([string]$file) {
|
|
82
|
+
return (Test-Path $file) -and (Select-String -Quiet -SimpleMatch 'Workflow discipline for Claude Code' $file)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# --- self-healing: drop machinery this version no longer installs into the target ---
|
|
86
|
+
# (thin model — migrates a target from an older, bloated install.) Gated on a prior forge
|
|
87
|
+
# install (.forge-manifest present) so a FIRST install never touches an unrelated project's
|
|
88
|
+
# own configs/ or skills/ dirs.
|
|
89
|
+
if (Test-Path (Join-Path $Target '.forge-manifest')) {
|
|
90
|
+
# Detect a genuinely OLD (pre-thin) forge install by its machinery; only then migrate
|
|
91
|
+
# configs/skills, so a routine re-install never relocates an app's own top-level dirs.
|
|
92
|
+
$oldInstall = $false
|
|
93
|
+
foreach ($f in 'sync.sh', 'sync.ps1', 'state.template.md', 'PROJECT.template.md', 'CONTINUITY.template.md', 'docs/extending.md') {
|
|
94
|
+
if (Test-Path (Join-Path $Target $f)) { $oldInstall = $true }
|
|
95
|
+
}
|
|
96
|
+
foreach ($f in 'sync.sh', 'sync.ps1', 'state.template.md', 'PROJECT.template.md', 'CONTINUITY.template.md', 'docs/extending.md') {
|
|
97
|
+
$p = Join-Path $Target $f
|
|
98
|
+
if (Test-Path $p) { Remove-Item -Force $p; Write-Host " - removed obsolete framework file: $f" }
|
|
99
|
+
}
|
|
100
|
+
# configs/ and neutral skills/ may hold pre-forge user edits — back up rather than delete.
|
|
101
|
+
$tConfigs = Join-Path $Target 'configs'
|
|
102
|
+
if ($oldInstall -and (Test-Path -PathType Container $tConfigs)) {
|
|
103
|
+
$bak = Join-Path $Target 'configs.pre-forge.bak'
|
|
104
|
+
if (Test-Path $bak) { Remove-Item -Recurse -Force $bak }
|
|
105
|
+
Move-Item $tConfigs $bak
|
|
106
|
+
Write-Host " ! configs/ is obsolete (engine configs are generated now) -> configs.pre-forge.bak; per-project Claude tweaks go in .claude/settings.local.json"
|
|
107
|
+
}
|
|
108
|
+
$tSkills = Join-Path $Target 'skills'
|
|
109
|
+
if ($oldInstall -and (Test-Path -PathType Container $tSkills)) {
|
|
110
|
+
$bak = Join-Path $Target 'skills.pre-forge.bak'
|
|
111
|
+
if (Test-Path $bak) { Remove-Item -Recurse -Force $bak }
|
|
112
|
+
Move-Item $tSkills $bak
|
|
113
|
+
Write-Host " ! neutral skills/ is obsolete (skills are generated now) -> skills.pre-forge.bak; add custom skills to the codeforge repo"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# --- MANAGED: CLAUDE.md (back up a pre-existing, non-forge one) ---
|
|
118
|
+
$tClaude = Join-Path $Target 'CLAUDE.md'
|
|
119
|
+
if ((Test-Path $tClaude) -and -not (Has-ForgeMarker $tClaude)) {
|
|
120
|
+
Copy-Item $tClaude "$tClaude.pre-forge.bak" -Force
|
|
121
|
+
Write-Host " ! backed up existing CLAUDE.md -> CLAUDE.md.pre-forge.bak (move project-specifics into PROJECT.md)"
|
|
122
|
+
}
|
|
123
|
+
Copy-Item (Join-Path $Payload 'CLAUDE.md') $tClaude -Force
|
|
124
|
+
|
|
125
|
+
# --- MANAGED: framework shared/rules/ (per-entry overwrite by name) ---
|
|
126
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $Target 'shared/rules') | Out-Null
|
|
127
|
+
$newRules = @(Get-ChildItem -File (Join-Path $Payload 'shared/rules') -Filter *.md).Name
|
|
128
|
+
|
|
129
|
+
# Prune framework rules removed upstream (see install.sh for rationale). Project-owned rules
|
|
130
|
+
# aren't in the manifest, so they're untouched. No manifest yet = skip prune.
|
|
131
|
+
$manifest = Join-Path $Target '.forge-manifest'
|
|
132
|
+
if (Test-Path $manifest) {
|
|
133
|
+
foreach ($line in Get-Content $manifest) {
|
|
134
|
+
if ($line -like 'rule:*') {
|
|
135
|
+
$n = $line.Substring(5)
|
|
136
|
+
# Treat the committed manifest as untrusted: a prune target must be a bare *.md filename,
|
|
137
|
+
# never a path/traversal, so a tampered entry can't delete outside shared/rules/.
|
|
138
|
+
if ($n -match '[\\/]' -or $n -match '\.\.' -or $n -eq '' -or $n -notmatch '\.md$') {
|
|
139
|
+
[Console]::Error.WriteLine(" ! ignoring unsafe manifest rule entry: $n")
|
|
140
|
+
} elseif ($newRules -notcontains $n) {
|
|
141
|
+
Remove-Item -Force (Join-Path $Target "shared/rules/$n") -ErrorAction SilentlyContinue
|
|
142
|
+
Write-Host " - pruned framework rule removed upstream: $n"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
foreach ($f in Get-ChildItem -File (Join-Path $Payload 'shared/rules') -Filter *.md) {
|
|
149
|
+
Copy-Item $f.FullName (Join-Path $Target "shared/rules/$($f.Name)") -Force
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Record the framework-owned manifest for the next upgrade's prune (rules only).
|
|
153
|
+
Set-Content -Path $manifest -Value (@($newRules | ForEach-Object { "rule:$_" }))
|
|
154
|
+
|
|
155
|
+
# Stamp the version that produced this install, for drift detection on the next run.
|
|
156
|
+
Set-Content -Path (Join-Path $Target '.forge-version') -Value $forgeVersion
|
|
157
|
+
|
|
158
|
+
# --- MANAGED: workflow state template (in shared/) ---
|
|
159
|
+
Copy-Item (Join-Path $Payload 'shared/state.template.md') (Join-Path $Target 'shared/state.template.md') -Force
|
|
160
|
+
|
|
161
|
+
# --- MANAGED: framework shared/scripts/ (agent-invoked Tier-B helpers, e.g. check-gates) ---
|
|
162
|
+
$scriptsSrc = Join-Path $Payload 'shared/scripts'
|
|
163
|
+
if (Test-Path -LiteralPath $scriptsSrc -PathType Container) {
|
|
164
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $Target 'shared/scripts') | Out-Null
|
|
165
|
+
foreach ($f in Get-ChildItem -File $scriptsSrc) {
|
|
166
|
+
Copy-Item $f.FullName (Join-Path $Target "shared/scripts/$($f.Name)") -Force
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# --- MANAGED: docs/ scaffolding ---
|
|
171
|
+
New-Item -ItemType Directory -Force -Path (Join-Path $Target 'docs') | Out-Null
|
|
172
|
+
foreach ($d in 'prds', 'plans', 'research', 'solutions', 'adr', 'e2e/reports', 'e2e/use-cases') {
|
|
173
|
+
$dd = Join-Path $Target "docs/$d"
|
|
174
|
+
New-Item -ItemType Directory -Force -Path $dd | Out-Null
|
|
175
|
+
$gk = Join-Path $dd '.gitkeep'
|
|
176
|
+
if (-not (Test-Path $gk)) { New-Item -ItemType File -Path $gk | Out-Null }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# --- PROJECT-OWNED: PROJECT.md / CONTINUITY.md / docs/CHANGELOG.md (create only if missing) ---
|
|
180
|
+
$tProject = Join-Path $Target 'PROJECT.md'
|
|
181
|
+
if (-not (Test-Path $tProject)) {
|
|
182
|
+
Copy-Item (Join-Path $Payload 'PROJECT.template.md') $tProject
|
|
183
|
+
Write-Host " + created PROJECT.md (fill in persona/info/variables/special rules)"
|
|
184
|
+
}
|
|
185
|
+
$tCont = Join-Path $Target 'CONTINUITY.md'
|
|
186
|
+
if (-not (Test-Path $tCont)) { Copy-Item (Join-Path $Payload 'CONTINUITY.template.md') $tCont }
|
|
187
|
+
$tChangelog = Join-Path $Target 'docs/CHANGELOG.md'
|
|
188
|
+
if (-not (Test-Path $tChangelog)) { Copy-Item (Join-Path $Payload 'docs/CHANGELOG.md') $tChangelog }
|
|
189
|
+
|
|
190
|
+
# --- back up any pre-existing, NON-forge per-engine skills dir before sync overwrites it ---
|
|
191
|
+
foreach ($eng in '.claude', '.agents') {
|
|
192
|
+
$sd = Join-Path $Target "$eng/skills"
|
|
193
|
+
if ((Test-Path $sd) -and -not (Test-Path (Join-Path $sd '.forge-generated'))) {
|
|
194
|
+
Move-Item $sd "$sd.pre-forge.bak"
|
|
195
|
+
Write-Host " ! backed up existing $eng/skills -> $eng/skills.pre-forge.bak (add custom skills to the codeforge repo)"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
# back up a real, non-forge AGENTS.md before sync overwrites it
|
|
199
|
+
$tAgents = Join-Path $Target 'AGENTS.md'
|
|
200
|
+
if ((Test-Path $tAgents) -and -not (Has-ForgeMarker $tAgents)) {
|
|
201
|
+
Copy-Item $tAgents "$tAgents.pre-forge.bak" -Force
|
|
202
|
+
Write-Host " ! backed up existing AGENTS.md -> AGENTS.md.pre-forge.bak"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# --- GENERATE engine dirs + AGENTS.md + opencode.json via sync (reads the codeforge source,
|
|
206
|
+
# writes straight into the target) ---
|
|
207
|
+
& (Join-Path $Src 'src/sync.ps1') -Out $Target | Out-Null
|
|
208
|
+
|
|
209
|
+
# --- Claude Code .claude/settings.local.json: auto-isolation + opt-in gate hook ---
|
|
210
|
+
# Auto-isolation (default; -NoIsolate to keep inheritance) adds claudeMdExcludes so Claude Code
|
|
211
|
+
# doesn't blend ancestor CLAUDE.md/.claude/rules into this project (Codex/OpenCode already scope
|
|
212
|
+
# to the project root). -WithHooks adds the Tier-C PreToolUse gate. Both land in the one
|
|
213
|
+
# gitignored file; codeforge only (re)writes it when absent or a prior install owned it.
|
|
214
|
+
$excludes = @()
|
|
215
|
+
if (-not $NoIsolate) {
|
|
216
|
+
$d = Split-Path -Parent $Target
|
|
217
|
+
$homeDir = [System.IO.Path]::GetFullPath($HOME)
|
|
218
|
+
while ($d -and $d -ne (Split-Path -Parent $d)) {
|
|
219
|
+
if (Test-Path -LiteralPath (Join-Path $d 'CLAUDE.md') -PathType Leaf) { $excludes += (Join-Path $d 'CLAUDE.md') }
|
|
220
|
+
if (Test-Path -LiteralPath (Join-Path $d 'CLAUDE.local.md') -PathType Leaf) { $excludes += (Join-Path $d 'CLAUDE.local.md') }
|
|
221
|
+
if ((Test-Path -LiteralPath (Join-Path $d '.claude/rules') -PathType Container) -and ($d -ne $homeDir)) { $excludes += (Join-Path $d '.claude/rules/**') }
|
|
222
|
+
$d = Split-Path -Parent $d
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
$sl = Join-Path $Target '.claude/settings.local.json'
|
|
227
|
+
if ($excludes.Count -gt 0 -or $WithHooks) {
|
|
228
|
+
if ((Test-Path -LiteralPath $sl -PathType Leaf) -and (-not $priorLocalManaged)) {
|
|
229
|
+
Write-Host " ! .claude/settings.local.json exists and isn't codeforge-managed — not touching it."
|
|
230
|
+
Write-Host " (skipped auto-isolation / gate hook; remove that file and re-run, or edit it by hand.)"
|
|
231
|
+
} else {
|
|
232
|
+
$settings = [ordered]@{}
|
|
233
|
+
if ($excludes.Count -gt 0) { $settings['claudeMdExcludes'] = $excludes }
|
|
234
|
+
if ($WithHooks) {
|
|
235
|
+
$settings['hooks'] = [ordered]@{
|
|
236
|
+
PreToolUse = @(
|
|
237
|
+
[ordered]@{
|
|
238
|
+
matcher = 'Bash'
|
|
239
|
+
hooks = @([ordered]@{ type = 'command'; command = 'pwsh -NoProfile -File "$env:CLAUDE_PROJECT_DIR/shared/scripts/claude-gate-hook.ps1"' })
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
$settings | ConvertTo-Json -Depth 10 | Set-Content -Path $sl
|
|
245
|
+
if (-not (Select-String -LiteralPath $mf -Pattern '^localsettings:managed$' -Quiet)) { Add-Content -Path $mf -Value 'localsettings:managed' }
|
|
246
|
+
if ($excludes.Count -gt 0) { Write-Host " + auto-isolated Claude Code from $($excludes.Count) ancestor instruction path(s) -> .claude/settings.local.json (-NoIsolate to keep inheritance)" }
|
|
247
|
+
if ($WithHooks) { Write-Host " + Claude gate hook -> .claude/settings.local.json (opt-in, hard-blocks ship on incomplete gates)" }
|
|
248
|
+
}
|
|
249
|
+
} elseif ($priorLocalManaged -and (Test-Path -LiteralPath $sl -PathType Leaf)) {
|
|
250
|
+
Remove-Item -LiteralPath $sl -Force
|
|
251
|
+
Write-Host " - removed codeforge-managed .claude/settings.local.json (nothing to configure now)"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# --- .gitignore (merge, don't clobber): ONLY local state (generated files are committed) ---
|
|
255
|
+
$gi = Join-Path $Target '.gitignore'
|
|
256
|
+
if (-not (Test-Path $gi)) { New-Item -ItemType File -Path $gi | Out-Null }
|
|
257
|
+
$marker = '# codeforge (local state — do not commit)'
|
|
258
|
+
if (-not (Select-String -Quiet -SimpleMatch $marker $gi)) {
|
|
259
|
+
$block = @"
|
|
260
|
+
|
|
261
|
+
# codeforge (local state — do not commit)
|
|
262
|
+
.DS_Store
|
|
263
|
+
.workflow/
|
|
264
|
+
.claude/settings.local.json
|
|
265
|
+
"@
|
|
266
|
+
Add-Content -Path $gi -Value $block
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# --- warn if the generated config lacks the forge push/PR gate ---
|
|
270
|
+
function Warn-Gate([string]$rel, [string]$needle, [string]$hint) {
|
|
271
|
+
$f = Join-Path $Target $rel
|
|
272
|
+
if ((Test-Path $f) -and -not (Select-String -Quiet -SimpleMatch $needle $f)) {
|
|
273
|
+
Write-Host " ! $rel has no forge push/PR gate ($hint) — add it to the codeforge source, then re-run."
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
Warn-Gate '.claude/settings.json' 'git push' 'ask-tier on git push / gh pr create'
|
|
277
|
+
Warn-Gate '.codex/config.toml' 'approval_policy' 'approval_policy'
|
|
278
|
+
Warn-Gate 'opencode.json' 'git push' 'permission.bash git push* / gh pr create*'
|
|
279
|
+
|
|
280
|
+
# --- post-install validation: generated skill copies + AGENTS.md must exist ---
|
|
281
|
+
$ok = $true
|
|
282
|
+
foreach ($p in '.claude/skills', '.agents/skills') {
|
|
283
|
+
if (-not (Test-Path (Join-Path $Target "$p/new-feature/SKILL.md"))) {
|
|
284
|
+
Write-Host " ! discovery FAILED: $p was not generated"; $ok = $false
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
foreach ($f in 'AGENTS.md', '.claude/settings.json', '.codex/config.toml', 'opencode.json', 'shared/state.template.md') {
|
|
288
|
+
if (-not (Test-Path (Join-Path $Target $f))) { Write-Host " ! FAILED: $f was not generated"; $ok = $false }
|
|
289
|
+
}
|
|
290
|
+
if (-not $ok) {
|
|
291
|
+
Write-Host " x install INCOMPLETE — issues above; NOT marking as installed"; exit 1
|
|
292
|
+
}
|
|
293
|
+
Write-Host " + validation: skills (.claude + .agents), AGENTS.md, and engine configs generated"
|
|
294
|
+
|
|
295
|
+
# --- git: the workflow (branches/commits) and ship gates operate on git ---
|
|
296
|
+
& git -C $Target rev-parse --is-inside-work-tree *> $null
|
|
297
|
+
if ($LASTEXITCODE -eq 0) {
|
|
298
|
+
# already a git repo — the workflow uses it
|
|
299
|
+
} elseif ($GitInit) {
|
|
300
|
+
& git -C $Target init -q
|
|
301
|
+
& git -C $Target add -A
|
|
302
|
+
& git -C $Target commit -q -m "chore: adopt codeforge" 2>$null
|
|
303
|
+
if ($LASTEXITCODE -eq 0) {
|
|
304
|
+
Write-Host " + initialized a git repo + baseline commit (chore: adopt codeforge)"
|
|
305
|
+
} else {
|
|
306
|
+
Write-Host " + initialized a git repo (baseline commit skipped — set git user.name/email, then commit)"
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
Write-Host " ! not a git repo — codeforge's workflow (branches, commits) and the ship gates assume git."
|
|
310
|
+
Write-Host " Run 'git init' here, or re-run the installer with -GitInit."
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Write-Host "codeforge installed."
|
|
314
|
+
Write-Host " next: (1) fill PROJECT.md (2) in Codex, trust the project when prompted"
|
|
315
|
+
Write-Host " (3) open the project in any of Claude Code / Codex / OpenCode"
|
|
316
|
+
Write-Host " to customize or upgrade: edit the codeforge source, then re-run this installer"
|
|
317
|
+
Write-Host " against the project (-Upgrade, or a bare re-run from inside it)."
|
package/install.sh
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# codeforge installer — copy the workflow discipline into a target project.
|
|
4
|
+
#
|
|
5
|
+
# ./install.sh [target-dir] [--upgrade] [--with-hooks] [--git-init] [--no-isolate]
|
|
6
|
+
#
|
|
7
|
+
# With no target-dir, installs into the current working directory. So the common flow is:
|
|
8
|
+
# cd my-project && /path/to/codeforge/install.sh
|
|
9
|
+
#
|
|
10
|
+
# THIN INSTALL: the target receives only what the agent needs at RUNTIME. All framework
|
|
11
|
+
# machinery (the neutral source in ./src/, the generators sync.sh/ps1, the generation
|
|
12
|
+
# inputs in configs/, and the seed templates) stays in the codeforge repo — never copied
|
|
13
|
+
# into the target. To customize or upgrade, edit the codeforge source and re-run this
|
|
14
|
+
# installer against the target (`--upgrade`, or a bare re-run from inside the project).
|
|
15
|
+
#
|
|
16
|
+
# The shippable payload is the NEUTRAL source in ./src/ (CLAUDE.md, skills/, shared/,
|
|
17
|
+
# configs/, docs/, *.template.md). This installer copies the runtime subset into the
|
|
18
|
+
# target, then runs `sync.sh --out <target>` to GENERATE each engine's config + skills
|
|
19
|
+
# (.claude/ + .agents/skills + .codex/config.toml + AGENTS.md + opencode.json) straight
|
|
20
|
+
# into the target. No symlinks (Windows-safe). The generated engine artifacts are COMMITTED
|
|
21
|
+
# with the target project so a fresh clone works immediately — no codeforge dependency at
|
|
22
|
+
# runtime; re-run the installer after editing the source to regenerate.
|
|
23
|
+
#
|
|
24
|
+
# LANDS IN THE TARGET (runtime only):
|
|
25
|
+
# CLAUDE.md, AGENTS.md, opencode.json, .claude/, .agents/, .codex/ (generated),
|
|
26
|
+
# shared/rules/*.md + shared/state.template.md + shared/scripts/* (managed), docs/ scaffolding + CHANGELOG,
|
|
27
|
+
# PROJECT.md + CONTINUITY.md (project-owned, seeded if missing).
|
|
28
|
+
# STAYS IN codeforge (never copied): src/skills (neutral), configs/, sync.sh, sync.ps1,
|
|
29
|
+
# *.template.md, docs/extending.md.
|
|
30
|
+
#
|
|
31
|
+
# MANAGED (framework baseline — OVERWRITTEN on install/upgrade): CLAUDE.md, the framework's
|
|
32
|
+
# OWN entries in shared/rules/, shared/state.template.md. Your own rules dropped into
|
|
33
|
+
# shared/rules/ survive upgrades (selective, per-entry by name).
|
|
34
|
+
# PROJECT-OWNED (created only if missing — NEVER clobbered): PROJECT.md, CONTINUITY.md,
|
|
35
|
+
# docs/CHANGELOG.md. Per-project Claude overrides go in .claude/settings.local.json.
|
|
36
|
+
#
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
|
|
39
|
+
SRC="$(cd "$(dirname "$0")" && pwd)"
|
|
40
|
+
PAYLOAD="$SRC/src"
|
|
41
|
+
FORGE_VERSION="unknown"
|
|
42
|
+
[ -f "$SRC/VERSION" ] && FORGE_VERSION="$(head -n1 "$SRC/VERSION" | tr -d '[:space:]')"
|
|
43
|
+
[ -n "$FORGE_VERSION" ] || FORGE_VERSION="unknown"
|
|
44
|
+
MODE="install"
|
|
45
|
+
WITH_HOOKS=0
|
|
46
|
+
GIT_INIT=0
|
|
47
|
+
ISOLATE=1 # auto-isolate Claude Code from ancestor CLAUDE.md by default (--no-isolate to keep inheritance)
|
|
48
|
+
TARGET=""
|
|
49
|
+
usage="usage: $0 [target-dir] [--upgrade] [--with-hooks] [--git-init] [--no-isolate]"
|
|
50
|
+
while [ $# -gt 0 ]; do
|
|
51
|
+
case "$1" in
|
|
52
|
+
--upgrade) MODE="upgrade" ;;
|
|
53
|
+
--with-hooks) WITH_HOOKS=1 ;;
|
|
54
|
+
--git-init) GIT_INIT=1 ;;
|
|
55
|
+
--no-isolate) ISOLATE=0 ;;
|
|
56
|
+
-*) echo "$usage (unknown arg: $1)" >&2; exit 2 ;;
|
|
57
|
+
*) if [ -z "$TARGET" ]; then TARGET="$1"; else echo "$usage (unexpected arg: $1)" >&2; exit 2; fi ;;
|
|
58
|
+
esac
|
|
59
|
+
shift
|
|
60
|
+
done
|
|
61
|
+
TARGET="${TARGET:-$PWD}"
|
|
62
|
+
|
|
63
|
+
[ -d "$TARGET" ] || { echo "error: target dir not found: $TARGET" >&2; exit 2; }
|
|
64
|
+
TARGET="$(cd "$TARGET" && pwd)"
|
|
65
|
+
# Did a prior forge install own .claude/settings.local.json? (read before the manifest is
|
|
66
|
+
# rewritten below, so the settings writer knows whether it may safely regenerate the file.)
|
|
67
|
+
PRIOR_LOCAL_MANAGED=0
|
|
68
|
+
grep -q '^localsettings:managed$' "$TARGET/.forge-manifest" 2>/dev/null && PRIOR_LOCAL_MANAGED=1
|
|
69
|
+
{ [ -f "$PAYLOAD/CLAUDE.md" ] && [ -d "$PAYLOAD/skills" ]; } || { echo "error: payload not found — run this from the codeforge repo" >&2; exit 2; }
|
|
70
|
+
[ "$TARGET" != "$SRC" ] || { echo "error: refusing to install into codeforge itself" >&2; exit 2; }
|
|
71
|
+
[ "$TARGET" != "$PAYLOAD" ] || { echo "error: refusing to install into the codeforge payload dir (src/)" >&2; exit 2; }
|
|
72
|
+
|
|
73
|
+
echo "codeforge $FORGE_VERSION → installing into: $TARGET (mode: $MODE)"
|
|
74
|
+
|
|
75
|
+
# --- version drift advisory (informational only, never blocks) ---
|
|
76
|
+
# Compare the version that last stamped this target against the one we're installing.
|
|
77
|
+
PRIOR_VERSION=""
|
|
78
|
+
[ -f "$TARGET/.forge-version" ] && PRIOR_VERSION="$(head -n1 "$TARGET/.forge-version" | tr -d '[:space:]')"
|
|
79
|
+
if [ -n "$PRIOR_VERSION" ] && [ "$PRIOR_VERSION" != "$FORGE_VERSION" ] \
|
|
80
|
+
&& [ "$FORGE_VERSION" != "unknown" ] && [ "$PRIOR_VERSION" != "unknown" ]; then
|
|
81
|
+
lower="$(printf '%s\n%s\n' "$PRIOR_VERSION" "$FORGE_VERSION" | sort -V | head -n1)"
|
|
82
|
+
if [ "$lower" = "$PRIOR_VERSION" ]; then
|
|
83
|
+
echo " ~ upgrading this target: codeforge $PRIOR_VERSION -> $FORGE_VERSION"
|
|
84
|
+
else
|
|
85
|
+
echo " ! this target was installed by a NEWER codeforge ($PRIOR_VERSION) than you're running ($FORGE_VERSION)."
|
|
86
|
+
echo " You may be downgrading it; teammates pinned to $PRIOR_VERSION could see drift. (advisory only)"
|
|
87
|
+
fi
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# --- self-healing: drop machinery this version no longer installs into the target ---
|
|
91
|
+
# (thin model — machinery lives in the codeforge repo; the target gets runtime only.) This
|
|
92
|
+
# migrates a target from an older, bloated install. Gated on a prior forge install
|
|
93
|
+
# (.forge-manifest present) so a FIRST install never touches an unrelated project's own
|
|
94
|
+
# configs/ or skills/ dirs.
|
|
95
|
+
if [ -f "$TARGET/.forge-manifest" ]; then
|
|
96
|
+
# Detect a genuinely OLD (pre-thin) forge install: it left this machinery at the target root.
|
|
97
|
+
# A modern thin install — or an unrelated app that happens to keep its own top-level configs/
|
|
98
|
+
# or skills/ — does NOT. Gate the configs/skills migration on this signal so a routine
|
|
99
|
+
# re-install never relocates a project's own configs/ or skills/ just because a manifest exists.
|
|
100
|
+
old_install=0
|
|
101
|
+
for f in sync.sh sync.ps1 state.template.md PROJECT.template.md CONTINUITY.template.md docs/extending.md; do
|
|
102
|
+
[ -e "$TARGET/$f" ] && old_install=1
|
|
103
|
+
done
|
|
104
|
+
# Framework-owned machinery, no user content — removed outright.
|
|
105
|
+
for f in sync.sh sync.ps1 state.template.md PROJECT.template.md CONTINUITY.template.md docs/extending.md; do
|
|
106
|
+
[ -e "$TARGET/$f" ] && { rm -f "$TARGET/$f"; echo " - removed obsolete framework file: $f"; }
|
|
107
|
+
done
|
|
108
|
+
# Only migrate configs/ and the neutral skills/ when this was actually an old bloated install
|
|
109
|
+
# (back them up rather than delete, so nothing is lost).
|
|
110
|
+
if [ "$old_install" = 1 ] && [ -d "$TARGET/configs" ]; then
|
|
111
|
+
rm -rf "$TARGET/configs.pre-forge.bak"; mv "$TARGET/configs" "$TARGET/configs.pre-forge.bak"
|
|
112
|
+
echo " ! configs/ is obsolete (engine configs are generated now) -> configs.pre-forge.bak; per-project Claude tweaks go in .claude/settings.local.json"
|
|
113
|
+
fi
|
|
114
|
+
if [ "$old_install" = 1 ] && [ -d "$TARGET/skills" ]; then
|
|
115
|
+
rm -rf "$TARGET/skills.pre-forge.bak"; mv "$TARGET/skills" "$TARGET/skills.pre-forge.bak"
|
|
116
|
+
echo " ! neutral skills/ is obsolete (skills are generated now) -> skills.pre-forge.bak; add custom skills to the codeforge repo"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- MANAGED: CLAUDE.md (back up a pre-existing, non-forge one on first install) ---
|
|
121
|
+
if [ -f "$TARGET/CLAUDE.md" ] && ! grep -q "Workflow discipline for Claude Code" "$TARGET/CLAUDE.md" 2>/dev/null; then
|
|
122
|
+
cp "$TARGET/CLAUDE.md" "$TARGET/CLAUDE.md.pre-forge.bak"
|
|
123
|
+
echo " ! backed up existing CLAUDE.md -> CLAUDE.md.pre-forge.bak (move project-specifics into PROJECT.md)"
|
|
124
|
+
fi
|
|
125
|
+
cp "$PAYLOAD/CLAUDE.md" "$TARGET/CLAUDE.md"
|
|
126
|
+
|
|
127
|
+
# --- MANAGED: framework shared/rules/ (per-entry overwrite by name) ---
|
|
128
|
+
# Refresh only the framework's own rule entries; anything else in shared/rules/ (your
|
|
129
|
+
# project's own rules) is left untouched, so it survives --upgrade.
|
|
130
|
+
mkdir -p "$TARGET/shared/rules"
|
|
131
|
+
new_rules="$(cd "$PAYLOAD/shared/rules" && ls *.md 2>/dev/null)"
|
|
132
|
+
|
|
133
|
+
# Prune framework rules removed upstream: anything in the last-install manifest that is no
|
|
134
|
+
# longer in the current payload is a framework rule deleted upstream — remove it. Project-owned
|
|
135
|
+
# rules are never in the manifest, so they are untouched. (No manifest yet = skip prune.)
|
|
136
|
+
manifest="$TARGET/.forge-manifest"
|
|
137
|
+
if [ -f "$manifest" ]; then
|
|
138
|
+
while IFS= read -r line; do
|
|
139
|
+
case "$line" in
|
|
140
|
+
rule:*)
|
|
141
|
+
n="${line#rule:}"
|
|
142
|
+
# The manifest is committed (and with --git-init, git-added), so treat it as untrusted:
|
|
143
|
+
# a prune target must be a bare *.md filename — never a path or traversal — so a tampered
|
|
144
|
+
# entry can't delete outside shared/rules/.
|
|
145
|
+
case "$n" in
|
|
146
|
+
*/*|*..*|"") echo " ! ignoring unsafe manifest rule entry: $n" >&2 ;;
|
|
147
|
+
*.md) printf '%s\n' "$new_rules" | grep -qxF "$n" || { rm -f "$TARGET/shared/rules/$n"; echo " - pruned framework rule removed upstream: $n"; } ;;
|
|
148
|
+
*) echo " ! ignoring non-.md manifest rule entry: $n" >&2 ;;
|
|
149
|
+
esac ;;
|
|
150
|
+
esac
|
|
151
|
+
done < "$manifest"
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
for f in "$PAYLOAD"/shared/rules/*.md; do
|
|
155
|
+
cp "$f" "$TARGET/shared/rules/$(basename "$f")"
|
|
156
|
+
done
|
|
157
|
+
|
|
158
|
+
# Record the framework-owned manifest for the next upgrade's prune (rules only; skills are
|
|
159
|
+
# fully generated by sync, so there is nothing skill-level to selectively prune).
|
|
160
|
+
{ printf '%s\n' "$new_rules" | sed 's/^/rule:/'; } > "$manifest"
|
|
161
|
+
|
|
162
|
+
# Stamp the version that produced this install, for drift detection on the next run.
|
|
163
|
+
printf '%s\n' "$FORGE_VERSION" > "$TARGET/.forge-version"
|
|
164
|
+
|
|
165
|
+
# --- MANAGED: workflow state template (lives in shared/, copied to .workflow/state.md at
|
|
166
|
+
# workflow start by the skills) ---
|
|
167
|
+
cp "$PAYLOAD/shared/state.template.md" "$TARGET/shared/state.template.md"
|
|
168
|
+
|
|
169
|
+
# --- MANAGED: framework shared/scripts/ (agent-invoked Tier-B helpers, e.g. check-gates) ---
|
|
170
|
+
if [ -d "$PAYLOAD/shared/scripts" ]; then
|
|
171
|
+
mkdir -p "$TARGET/shared/scripts"
|
|
172
|
+
for f in "$PAYLOAD"/shared/scripts/*; do
|
|
173
|
+
[ -e "$f" ] || continue
|
|
174
|
+
cp "$f" "$TARGET/shared/scripts/$(basename "$f")"
|
|
175
|
+
done
|
|
176
|
+
chmod +x "$TARGET"/shared/scripts/*.sh 2>/dev/null || true
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
# --- MANAGED: docs/ scaffolding ---
|
|
180
|
+
mkdir -p "$TARGET/docs"
|
|
181
|
+
for d in prds plans research solutions adr e2e/reports e2e/use-cases; do
|
|
182
|
+
mkdir -p "$TARGET/docs/$d"
|
|
183
|
+
[ -e "$TARGET/docs/$d/.gitkeep" ] || touch "$TARGET/docs/$d/.gitkeep"
|
|
184
|
+
done
|
|
185
|
+
|
|
186
|
+
# --- PROJECT-OWNED: PROJECT.md / CONTINUITY.md / docs/CHANGELOG.md (create only if missing) ---
|
|
187
|
+
[ -f "$TARGET/PROJECT.md" ] || { cp "$PAYLOAD/PROJECT.template.md" "$TARGET/PROJECT.md"; echo " + created PROJECT.md (fill in persona/info/variables/special rules)"; }
|
|
188
|
+
[ -f "$TARGET/CONTINUITY.md" ] || cp "$PAYLOAD/CONTINUITY.template.md" "$TARGET/CONTINUITY.md"
|
|
189
|
+
[ -f "$TARGET/docs/CHANGELOG.md" ] || cp "$PAYLOAD/docs/CHANGELOG.md" "$TARGET/docs/CHANGELOG.md"
|
|
190
|
+
|
|
191
|
+
# --- back up any pre-existing, NON-forge per-engine skills dir before sync overwrites it ---
|
|
192
|
+
# (forge-generated dirs carry a .forge-generated marker; a dir without it is the user's own,
|
|
193
|
+
# so we never wipe a user's skills — even one coincidentally named new-feature.)
|
|
194
|
+
for eng in .claude .agents; do
|
|
195
|
+
sd="$TARGET/$eng/skills"
|
|
196
|
+
if [ -e "$sd" ] && [ ! -e "$sd/.forge-generated" ]; then
|
|
197
|
+
mv "$sd" "$sd.pre-forge.bak"
|
|
198
|
+
echo " ! backed up existing $eng/skills -> $eng/skills.pre-forge.bak (add custom skills to the codeforge repo)"
|
|
199
|
+
fi
|
|
200
|
+
done
|
|
201
|
+
# back up a real, non-forge AGENTS.md before sync overwrites it
|
|
202
|
+
if [ -f "$TARGET/AGENTS.md" ] && ! grep -q "Workflow discipline for Claude Code" "$TARGET/AGENTS.md" 2>/dev/null; then
|
|
203
|
+
cp "$TARGET/AGENTS.md" "$TARGET/AGENTS.md.pre-forge.bak"
|
|
204
|
+
echo " ! backed up existing AGENTS.md -> AGENTS.md.pre-forge.bak"
|
|
205
|
+
fi
|
|
206
|
+
|
|
207
|
+
# --- GENERATE engine dirs + AGENTS.md + opencode.json via sync (reads the codeforge source,
|
|
208
|
+
# writes straight into the target — no source or sync script copied there) ---
|
|
209
|
+
bash "$PAYLOAD/sync.sh" --out "$TARGET" >/dev/null
|
|
210
|
+
|
|
211
|
+
# --- Claude Code .claude/settings.local.json: auto-isolation + opt-in gate hook ---
|
|
212
|
+
# Both features land in this one gitignored, per-developer, machine-specific file. Auto-isolation
|
|
213
|
+
# (default; --no-isolate to keep inheritance) adds `claudeMdExcludes` so Claude Code does NOT
|
|
214
|
+
# blend ancestor CLAUDE.md / .claude/rules into this project — Codex and OpenCode already scope
|
|
215
|
+
# to the project root, Claude Code walks to the filesystem root. --with-hooks adds the Tier-C
|
|
216
|
+
# PreToolUse gate. codeforge only (re)writes this file when it is absent or a prior forge install
|
|
217
|
+
# owned it (tracked as `localsettings:managed` in .forge-manifest); a file it doesn't own is left
|
|
218
|
+
# alone. The hook's $CLAUDE_PROJECT_DIR is resolved by Claude Code at runtime, not now.
|
|
219
|
+
excludes=""
|
|
220
|
+
if [ "$ISOLATE" = "1" ]; then
|
|
221
|
+
d="$(dirname "$TARGET")"
|
|
222
|
+
while [ -n "$d" ] && [ "$d" != "/" ]; do
|
|
223
|
+
[ -f "$d/CLAUDE.md" ] && excludes="$excludes$d/CLAUDE.md
|
|
224
|
+
"
|
|
225
|
+
[ -f "$d/CLAUDE.local.md" ] && excludes="$excludes$d/CLAUDE.local.md
|
|
226
|
+
"
|
|
227
|
+
{ [ -d "$d/.claude/rules" ] && [ "$d" != "$HOME" ]; } && excludes="$excludes$d/.claude/rules/**
|
|
228
|
+
"
|
|
229
|
+
nd="$(dirname "$d")"; [ "$nd" = "$d" ] && break; d="$nd"
|
|
230
|
+
done
|
|
231
|
+
fi
|
|
232
|
+
n_excl=$(printf '%s' "$excludes" | grep -c . || true)
|
|
233
|
+
|
|
234
|
+
sl="$TARGET/.claude/settings.local.json"
|
|
235
|
+
if [ "$n_excl" -gt 0 ] || [ "$WITH_HOOKS" = "1" ]; then
|
|
236
|
+
if [ -f "$sl" ] && [ "$PRIOR_LOCAL_MANAGED" != "1" ]; then
|
|
237
|
+
echo " ! .claude/settings.local.json exists and isn't codeforge-managed — not touching it."
|
|
238
|
+
echo " (skipped auto-isolation / gate hook; remove that file and re-run, or edit it by hand.)"
|
|
239
|
+
else
|
|
240
|
+
excl_json=""
|
|
241
|
+
while IFS= read -r p; do
|
|
242
|
+
[ -n "$p" ] || continue
|
|
243
|
+
if [ -z "$excl_json" ]; then excl_json="$(printf '\n "%s"' "$p")"
|
|
244
|
+
else excl_json="$excl_json$(printf ',\n "%s"' "$p")"; fi
|
|
245
|
+
done <<EOF
|
|
246
|
+
$excludes
|
|
247
|
+
EOF
|
|
248
|
+
hook_block=' "hooks": {
|
|
249
|
+
"PreToolUse": [
|
|
250
|
+
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "sh \"$CLAUDE_PROJECT_DIR/shared/scripts/claude-gate-hook.sh\"" } ] }
|
|
251
|
+
]
|
|
252
|
+
}'
|
|
253
|
+
{
|
|
254
|
+
printf '{'
|
|
255
|
+
[ "$n_excl" -gt 0 ] && printf '\n "claudeMdExcludes": [%s\n ]' "$excl_json"
|
|
256
|
+
{ [ "$n_excl" -gt 0 ] && [ "$WITH_HOOKS" = "1" ]; } && printf ','
|
|
257
|
+
[ "$WITH_HOOKS" = "1" ] && printf '\n%s' "$hook_block"
|
|
258
|
+
printf '\n}\n'
|
|
259
|
+
} > "$sl"
|
|
260
|
+
grep -q '^localsettings:managed$' "$manifest" 2>/dev/null || printf 'localsettings:managed\n' >> "$manifest"
|
|
261
|
+
[ "$n_excl" -gt 0 ] && echo " + auto-isolated Claude Code from $n_excl ancestor instruction path(s) -> .claude/settings.local.json (--no-isolate to keep inheritance)"
|
|
262
|
+
[ "$WITH_HOOKS" = "1" ] && echo " + Claude gate hook -> .claude/settings.local.json (opt-in, hard-blocks ship on incomplete gates)"
|
|
263
|
+
fi
|
|
264
|
+
elif [ "$PRIOR_LOCAL_MANAGED" = "1" ] && [ -f "$sl" ]; then
|
|
265
|
+
rm -f "$sl"
|
|
266
|
+
echo " - removed codeforge-managed .claude/settings.local.json (nothing to configure now)"
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
# --- .gitignore (merge, don't clobber): ONLY local state ---
|
|
270
|
+
# The generated engine artifacts (.claude/, .agents/, .codex/, AGENTS.md, opencode.json)
|
|
271
|
+
# are COMMITTED with the project so a fresh clone works immediately — no post-clone step,
|
|
272
|
+
# no dependency on codeforge. Only genuinely local/transient state is ignored here.
|
|
273
|
+
touch "$TARGET/.gitignore"
|
|
274
|
+
if ! grep -qx '# codeforge (local state — do not commit)' "$TARGET/.gitignore"; then
|
|
275
|
+
{
|
|
276
|
+
printf '\n# codeforge (local state — do not commit)\n'
|
|
277
|
+
printf '.DS_Store\n.workflow/\n.claude/settings.local.json\n'
|
|
278
|
+
} >> "$TARGET/.gitignore"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# --- warn if the generated config lacks the forge push/PR gate (points at the codeforge
|
|
282
|
+
# source baseline that produced it) ---
|
|
283
|
+
warn_gate() { # $1 = generated file in target, $2 = grep needle, $3 = hint
|
|
284
|
+
if [ -f "$TARGET/$1" ] && ! grep -q "$2" "$TARGET/$1" 2>/dev/null; then
|
|
285
|
+
echo " ! $1 has no forge push/PR gate ($3) — add it to the codeforge source, then re-run."
|
|
286
|
+
fi
|
|
287
|
+
}
|
|
288
|
+
warn_gate ".claude/settings.json" "git push" "ask-tier on git push / gh pr create"
|
|
289
|
+
warn_gate ".codex/config.toml" "approval_policy" "approval_policy"
|
|
290
|
+
warn_gate "opencode.json" "git push" "permission.bash git push* / gh pr create*"
|
|
291
|
+
|
|
292
|
+
# --- post-install validation: generated skills + AGENTS.md + engine configs must exist ---
|
|
293
|
+
ok=1
|
|
294
|
+
for p in .claude/skills .agents/skills; do
|
|
295
|
+
[ -e "$TARGET/$p/new-feature/SKILL.md" ] || { echo " ! discovery FAILED: $p was not generated"; ok=0; }
|
|
296
|
+
done
|
|
297
|
+
for f in AGENTS.md .claude/settings.json .codex/config.toml opencode.json shared/state.template.md; do
|
|
298
|
+
[ -f "$TARGET/$f" ] || { echo " ! FAILED: $f was not generated"; ok=0; }
|
|
299
|
+
done
|
|
300
|
+
if [ "$ok" != 1 ]; then
|
|
301
|
+
echo " ✗ install INCOMPLETE — issues above; NOT marking as installed" >&2
|
|
302
|
+
exit 1
|
|
303
|
+
fi
|
|
304
|
+
echo " ✓ validation: skills (.claude + .agents), AGENTS.md, and engine configs generated"
|
|
305
|
+
|
|
306
|
+
# --- git: the workflow (branches/commits) and ship gates operate on git ---
|
|
307
|
+
if git -C "$TARGET" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
308
|
+
: # already a git repo — the workflow uses it
|
|
309
|
+
elif [ "$GIT_INIT" = "1" ]; then
|
|
310
|
+
git -C "$TARGET" init -q
|
|
311
|
+
git -C "$TARGET" add -A
|
|
312
|
+
if git -C "$TARGET" commit -q -m "chore: adopt codeforge" 2>/dev/null; then
|
|
313
|
+
echo " + initialized a git repo + baseline commit (chore: adopt codeforge)"
|
|
314
|
+
else
|
|
315
|
+
echo " + initialized a git repo (baseline commit skipped — set git user.name/email, then commit)"
|
|
316
|
+
fi
|
|
317
|
+
else
|
|
318
|
+
echo " ! not a git repo — codeforge's workflow (branches, commits) and the ship gates assume git."
|
|
319
|
+
echo " Run 'git init' here, or re-run the installer with --git-init."
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
echo "codeforge installed."
|
|
323
|
+
echo " next: (1) fill PROJECT.md (2) in Codex, trust the project when prompted"
|
|
324
|
+
echo " (3) open the project in any of Claude Code / Codex / OpenCode"
|
|
325
|
+
echo " to customize or upgrade: edit the codeforge source, then re-run this installer"
|
|
326
|
+
echo " against the project (--upgrade, or a bare re-run from inside it)."
|